Question
Employees are organized into teams. Each team can have multiple employees, and each employee can belong to multiple teams. This many-to-many relationship is represented by the team_membership table.
Each project is assigned to one team. Projects are subdivided into tasks, and each task is assigned to an employee.
Is it possible to guarantee that a task's employee is a member of the corresponding project's team, without adding triggers or redundant columns?
Example tables
CREATE TABLE employee
(
employee_id bigserial PRIMARY KEY,
employee_name text
);
CREATE TABLE team
(
team_id bigserial PRIMARY KEY,
team_name text
);
CREATE TABLE team_membership
(
team_id bigint NOT NULL REFERENCES team,
employee_id bigint NOT NULL REFERENCES employee,
PRIMARY KEY (team_id, employee_id)
);
CREATE TABLE project
(
project_id bigserial PRIMARY KEY,
team_id bigint NOT NULL REFERENCES team,
project_name text
);
CREATE TABLE task
(
task_id bigserial PRIMARY KEY,
task_name text,
project_id bigint NOT NULL REFERENCES project,
employee_id bigint NOT NULL REFERENCES employee
);
What I have already tried
Use a trigger to check validity when data changes. This would require writing similar trigger procedures for the employee and team_membership tables.
CREATE FUNCTION check_employee_member_of_team() RETURNS trigger AS $$
BEGIN
IF NOT new.employee_id IN (
SELECT employee_id FROM team_membership tm
JOIN project pr ON tm.team_id = pr.team_id
WHERE pr.proejct_id = new.project_id
) THEN
RAISE EXCEPTION 'Employee is not a member of project';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER insert_or_update_task_trigger
BEFORE INSERT OR UPDATE ON task
FOR EACH ROW EXECUTE PROCEDURE check_employee_member_of_team();
Add a team_id column to the task table, and enforce the constraint using composite foreign keys. project.team_id and task.team_id are redundant.
CREATE TABLE project
(
project_id bigserial PRIMARY KEY,
team_id bigint NOT NULL REFERENCES team,
project_name text,
UNIQUE (project_id, team_id)
);
CREATE TABLE task
(
task_id bigserial PRIMARY KEY,
task_name text,
project_id bigint NOT NULL,
team_id bigint NOT NULL,
employee_id bigint NOT NULL REFERENCES employee,
FOREIGN KEY (project_id, team_id) REFERENCES project (project_id, team_id),
FOREIGN KEY (team_id, employee_id) REFERENCES team_membership (team_id, employee_id)
);