Using PostgreSQL composite foreign keys to prevent cross organization associations - postgresql

I am considering using composite foreign keys in PostgreSQL to ensure that rows which belongs to different organizations cannot be associated with each other. Is this a good idea, or will I live to regret it?
For example, in a system which has addresses and shipments, both of which belong directly to an organization, if a shipment has an address, with composite foreign key constraints I could limit that the address must belong to the same organization.
So simplified, I would have something like this:
CREATE TABLE organizations (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL,
FOREIGN KEY (organization_id) REFERENCES organizations (id),
UNIQUE (id, organization_id)
);
CREATE TABLE shipments (
id SERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL,
address_id INTEGER NOT NULL,
FOREIGN KEY (organization_id) REFERENCES organizations (id),
FOREIGN KEY (address_id, organization_id) REFERENCES addresses (id, organization_id),
UNIQUE (id, organization_id)
);
On the surface this seems good, since it ensures better data integrity, but possibly insertion could become slow, and maybe there are other issues I am not foreseeing?

Related

Validating foreign key through tables in postgres

I'm trying to validate a foreign key constraint on my table through another table, but I'm not sure how to go about it, or even the correct nomenclature to google.
I have a User table:
create table User ( id uuid primary key );
Each user can have multiple stores:
create table Store(
id uuid primary key,
user_id uuid foreign key store_user_id_fk references User (id)
);
and each user+store can have products
create table Product(
id uuid primary key,
user_id uuid foreign key product_user_id_fk references User (id),
store_id uuid foreign key product_sotre_id_fk references Store (id),
)
My question is: how can I write a constraint on Product such that any (user_id,store_id) combination also must have a valid entry in the Store table? The case I'm trying to prevent is an entry being added to Product where the store does not also belong to the user.
Is there some way of adding:
CHECK ( store_id == Store.id and user_id == Store.user_id )
To the product table?
Unless I'm misunderstanding your question, I'm pretty sure it would just be:
create table Product(
id uuid primary key,
user_id uuid foreign key product_user_id_fk references User (id),
store_id uuid foreign key product_sotre_id_fk references Store (id),
FOREIGN KEY (user_id, store_id) REFERENCES store(user_id, id)
);
This would indicate that the primary key of store should be (id, user_id) not just id or, at at minimum, it should have a UNIQUE constraint on (id, user_id).

Do I need to declare a unique constraint for `bigint generated always as identity`?

I'm creating a multi-tenant application and am prepending the tenant_id to all tables that my tenants will access. All of the tables will also have an incrementing surrogate key. Will I need to declare a unique constraint of the surrogate key or is that redundant?
CREATE TABLE tenant (
primary key (tenant_id),
tenant_id bigint generated always as identity
);
CREATE TABLE person (
primary key (tenant_id, person_id)
person_id bigint generated always as identity,
tenant_id bigint not null,
unique (person_id), -- Do I need this?
foreign key (tenant_id) references tenant
);
The primary key of a table should be a minimal set of columns that uniquely identify a table row. So that should be person_id, as it was specifically created for that purpose.
Add another (non-unique) index on tenant_id or (tenant_id, person_id) if you need to speed up searches based on tenant_id.

How to use constraints on ranges with a junction table?

Based on the documentation it's pretty straightforward how to prevent any overlapping reservations in the table at the same time.
CREATE EXTENSION btree_gist;
CREATE TABLE room_reservation (
room text,
during tsrange,
EXCLUDE USING GIST (room WITH =, during WITH &&)
);
However, when you have multiple resources that can be reserved by users, what is the best approach to check for overlappings? You can see below that I want to have users reserve multiple resources. That's why I'm using the junction table Resources_Reservations. Is there any way I can use EXCLUDE in order to check that no resources are reserved at the same time?
CREATE TABLE Users(
id serial primary key,
name text
);
CREATE TABLE Resources(
id serial primary key,
name text
);
CREATE TABLE Reservations(
id serial primary key,
duration tstzrange,
user_id serial,
FOREIGN KEY (user_id) REFERENCES Users(id)
);
CREATE TABLE Resources_Reservations(
resource_id serial,
reservation_id serial,
FOREIGN KEY (resource_id) REFERENCES Resources(id),
FOREIGN KEY (reservation_id) REFERENCES Reservations(id),
PRIMARY KEY (resource_id, reservation_id)
);
I think what you want is doable with a slight model change.
But first let's correct a misconception. You have foreign key columns (user_id, resource_id, etc) defined as SERIAL. This is incorrect, they should be INTEGER. This is because SERIAL is not actually a data type. It is a psuedo-data type that is actually a shortcut for: creating a sequence, creating a column of type integer, and defining the sequence created as the default value. With that out of the way.
I think your Resources_Reservations is redundant. A reservation is by a user, but a reservation without something reserved would just be user information. Bring the resource_id into Reservation. Now a Reservation is by a user for a resource with a duration. Everything your current model contains but less complexity.
Assuming you don't have data that needs saving, then:
create table users(
id serial primary key,
name text
);
create table resources(
id serial primary key,
name text
);
create table reservations(
user_id integer
resource_id integer
duration tstzrange,
foreign key (user_id) references users(id)
foreign key (resource_id) references resources(id),
primary key (resource_id, user_id)
);
You should now be able to create your GIST exclusion.

Will a primary key index serve as an index for a foreign key when fk columns are subset of pk?

I have a table where part of the primary key is a foreign key to another table.
create table player_result (
event_id integer not null,
pub_time timestamp not null,
name_key varchar(128) not null,
email_address varchar(128),
withdrawn boolean not null,
place integer,
realized_values hstore,
primary key (event_id, pub_time, name_key),
foreign key (email_address) references email(address),
foreign key (event_id, pub_time) references event_publish(event_id, pub_time));
Will the index generated for the primary key suffice to back the foreign key on event_id and pub_time?
Yes.
Index A,B,C
is good for:
A
A,B
A,B,C (and any other combination of the full 3 fields, if default order is unimportant)
but not good for other combinations (such as B,C, C,A etc.).
It will be useful for the referencing side, such that a DELETE or UPDATE on the referenced table can use the PRIMARY KEY of the referencing side as an index when performing checks for the existence of referencing rows or running cascade update/deletes. PostgreSQL doesn't require this index to exist at all, it just makes foreign key constraint checks faster if it is there.
It is not sufficient to serve as the unique constraint for a reference to those columns. You couldn't create a FOREIGN KEY that REFERENCES player_result(event_id, pub_time) because there is no unique constraint on those columns. That pair can appear multiple times in the table so long as each pair has a different name_key.
As #xagyg accurately notes, the unique b-tree index created by the foreign key reference is also only useful for references to columns from the left of the index. It could not be used for a lookup of pub_time, name_key or just name_key, for example.

How to make a Foreigne key that references NOT Primary or Unique column from another table?

I have made classical Employee - Employer table that has one primary key and one foreign key. Foreign key references primary so it is a self referencing table:
CREATE TABLE Worker
(
OIB NUMERIC(2,0),
Name NVARCHAR(10),
Surname NVARCHAR(20),
DateOfEmployment DATETIME2 NOT NULL,
Adress NVARCHAR(20),
City NVARCHAR(10),
SUPERIOR NUMERIC(2,0) UNIQUE,
Constraint PK_Worker PRIMARY KEY(OIB),
CONSTRAINT FK_Worker FOREIGN KEY (Superior) REFERENCES Worker(OIB)
);
Second table that should keep points for all my employees is made like this:
CREATE TABLE Point
(
OIB_Worker NUMERIC(2,0) NOT NULL,
OIB_Superior NUMERIC(2,0) NOT NULL,
Pt_To_Worker tinyint,
Pt_To_Superior tinyint,
Month_ INT NOT NULL,
Year_ INT NOT NULL,
CONSTRAINT FK_Point_Worker FOREIGN KEY (OIB_Worker) References Worker(OIB),
CONSTRAINT FK_Point_Worker_2 FOREIGN KEY (OIB_Superior) References Worker(Superior),
CONSTRAINT PK_Point PRIMARY KEY(OIB_Worker,OIB_Superior,Month_,Year_)
);
It should enable storing grades per month for every employee.
That is, I have two foreign keys, one for Worker.OIB and one for Worker.Superior. Also, I have a composite primary key made of columns Point.OIB_Worker, Point.OIB_superior, Point.Month_ and Point.Year_. Key is composite because it needs to disable entering grades more then once a month.
My question is:
How to make a foreign key from Point to Worker so that any superior can have more then one employee assigned to him?
If you look closely, my implementation works but it can have only one employee per manager.
That is because of a fact that a foreign key has to reference either the primary or the unique column from other table. And my Worker.Superior is UNIQUE, so it can have only unique values (no repetition).
I think many people will find this example interesting as it is a common problem when making a new database.
I think your FK_Point_Worker_2 should also have References Worker(OIB), and you should remove the UNIQUE constraint from Worker.Superior. That way a superior can have more than one worker assigned to him.
Think about it. You have unique constraint on SUPERIOR and you are confused as to why two employees cannot have the same SUPERIOR. That is what a unique constraint does - not allow duplicates.
A FK can only reference a unique column or columns.
A FK_Point_Worker_2 with a References Worker(OIB) does not assure OIB is a Superior.
I would add a unique constraint on Worker on (OIB, SUPERIOR)
and remove the unique constraint on SUPERIOR.
It will always be unique as OIB is unique.
Then have composite FK relationship.
This is an example of a composite FK relationship
ALTER TABLE [dbo].[wfBchFolder] WITH CHECK ADD CONSTRAINT [FK_wfBchFolder_wfBch] FOREIGN KEY([wfID], [bchID])
REFERENCES [dbo].[WFbch] ([wfID], [ID])
GO