I am building a project with an organization chart using Codeigniter + MySQL + Active Record.
There are departments listed as organization tree, Staff for information of the persons, Staff Roles and Staff_Departments where i store the matching:
Department - Staff - Role
You can see the structure below:
Departments (parent_id is used to build the tree)
Staff (raw Staff information)
Staff Roles (the lowest the weight, the highest in hierarchy)
Staff Departments (In which department - Who - What Role)
In a later phase, a Staff will probably belong in 2 or more departments with different roles. That's why i used a separate table Staff_departments for many-to-many. In this case, let's keep it simple and assume that 1 Staff belongs to 1 Department.
What i am trying to do:
A Manager(role weight=0 || role_id=1) in a Department, can view the Staff(Employees AND Supervisors) who work in his Department AND all the Staff(Employees AND Supervisors) from the Departments that are Children of his Department. The depth of the tree is unknown.
A Supervisor can view the Staff(only Employees) who work only in his Department.
An Employee can only view himself.
For Supervisors and Employees, the process is simple, so i think i am ok with that. For Managers probably i have to do something recursive, but i'm struggling every time i'm starting writing some lines of code.
My idea is to have a function find_related_staff($staff_id) {} in my controller, which i will pass the ID of the Staff who is logged in and it will return an array with the IDs of his related Staff. The only thing i got, is the ID of the Staff who is logged in.
If Manager return IDs of Managers,Supervisors and Employees related in his Department AND Managers,Supervisors and Employees from
Child Departments of his Department.
If Supervisor return IDs of Supervisors and Employees related in his Department only.
If Employee return his ID
Any idea on how to achieve that?
Ok, I think that, in order to make things easier to understand, we need to break your problem into small pieces (and I'm only focus on the section that you say you really need help: the managers' recursion).
First, we get the current department associated with the user authenticated. As you've said, you only have the ID of the staff currently sign so we'll start with that. Let's say the user id is assigned to variable $user_id.
$user_department = $this->db->get_where('staff_departments', ['staff_id' => $user_id])->row();
Now that we have the department, we check to see what's the role of the user in that department. We'll add that info to the $user_department object:
$user_department->role = $this->db->get_where('staff_roles', ['role_id' => $user_department->role_id])->row();
Let's check the weight of the user's role, shall we? If it's 0, we know it's a manager on that department so we'll recursively find the nested departments and their staff information. As per your logic dictates, we can check, here, if the user is a supervisor, also, and escalate if necessary. Like this:
if ($user_department->role->role_weight <= 1) {
// the user is a supervisor OR a manager, but both those can see, at least, the current department's staff information
$user_department->staff = $this->db->get_where('staff_departments', ['department_id' => $user_department->department_id]);
// now is the user a manager? If so, let's find nested departments
if ($user_department->role->role_weight === 0) {
$user_department->childs = $this->getChildDepartmentsAndStaffOf($user_department->department_id);
}
}
As you may note, there's a function that'll be called recursively. It must be something along this lines:
public function getChildDepartmentsAndStaffOf($department_id)
{
$child_departments = $this->db->get_where('departments', ['parent_id' => $department_id]);
if (! $child_departments) {
return null;
}
foreach ($child_departments as &$department) {
$department->staff = $this->db->get_where('staff_departments', ['department_id' => $department->department_id]);
$department->childs = $this->getChildDepartmentsAndStaffOf($department->department_id);
}
return $child_departments;
}
Now, you have the structure you want. I know this might be refactored, but I think that is enough to get your answer and point you to the right path.
Hope I've helped a little.
Yes, to get it done you must use recursive procedures. (I'm using MySQL 5.6.19)
I have created some test data before the stored procedures:
Sample data based on your question requirements:
create table departments
(
id int not null primary key auto_increment,
parent_id int,
department_name varchar(100)
);
insert into departments (id,parent_id,department_name)
values
(1,0,'Test A'),
(2,1,'Test B'),
(3,2,'Test C');
create table staff
(
id int not null primary key auto_increment,
ip_address varchar(100),
username varchar(100)
);
insert into staff values
(1,'127.0.0.1','ats'),
(2,'127.0.0.1','admin'),
(3,'127.0.0.1','george'),
(4,'127.0.0.1','jhon')
;
create table staff_roles
(
role_id int not null primary key auto_increment,
role_name varchar(100),
role_height int
);
insert into staff_roles values
(1,'Manager',0),
(2,'Supervisor',1),
(3,'Employee',2)
;
create table staff_departments
(
staff_department_id int not null primary key auto_increment,
department_id int,
staff_id int,
role_id int
);
insert into staff_departments values
(1,1,2,1),
(2,2,1,2),
(3,3,3,3),
(4,3,4,3);
It's time to create the stored procedures:
find_related_staff is the procedure that receives the staff_id parameter, according to that value will find the role_id in staff_departments table.
The variable #result will accumulate the final result as comma separated values.
find_recursive is the procedure that search in child departments and get the staff_id into #result variable;
The procedure code:
delimiter $$
drop procedure if exists find_related_staff$$
create procedure find_related_staff(p_id int)
begin
declare p_role_id int;
declare p_department_id int;
declare p_return varchar(255) default '';
declare p_role varchar(100);
select d.role_id, d.department_id, r.role_name
into p_role_id,p_department_id, p_role
from staff_departments d
inner join staff_roles r on d.role_id = r.role_id
where d.staff_id = p_id
limit 1;
case p_role_id
when 3 then -- employee (return the same id)
set #result = p_id;
when 2 then -- supervisor
select group_concat(s.staff_id)
into #result
from staff_departments s
where
s.role_id = 3
and s.department_id in
( select d.id
from departments d
where d.parent_id = p_department_id )
and s.role_id <> p_id;
when 1 then -- manager (complex recursive query)
select coalesce(group_concat(s.staff_id),'')
into #result
from staff_departments s
where
s.department_id = p_department_id
and s.staff_id <> p_id;
-- here we go!
call find_recursive(p_department_id);
end case;
select #result as result, p_role as role;
end $$
delimiter ;
delimiter $$
drop procedure if exists find_recursive$$
create procedure find_recursive(p_dept_id int)
begin
declare done int default false;
declare p_department int default false;
declare tmp_result varchar(255) default '';
-- cursor for all depend departments
declare c_departments cursor for
select s.department_id
from staff_departments s
where
s.department_id in
( select d.id
from departments d
where d.parent_id = p_dept_id );
declare continue handler for not found set done = true;
-- getting current departmens
set tmp_result =
(select coalesce(group_concat(s.staff_id),'')
from staff_departments s
where
s.department_id in
( select d.id
from departments d
where d.parent_id = p_dept_id ));
if length(tmp_result) > 0 then
if length(#result) > 0 then
set #result = concat(#result,',',tmp_result);
else
set #result = tmp_result;
end if;
open c_departments;
read_loop: loop
fetch c_departments into p_department;
if done then
leave read_loop;
end if;
call find_recursive(p_department);
end loop;
close c_departments;
end if;
end $$
delimiter ;
Testing:
Important: The max deep in recursion is 0 as default, we must change that value:
SET max_sp_recursion_depth=255;
Now we have the bellow configuration on your staff_departments table:
+---------------------+---------------+----------+---------+
| staff_department_id | department_id | staff_id | role_id |
+---------------------+---------------+----------+---------+
| 1 | 1 | 2 | 1 |
| 2 | 2 | 1 | 2 |
| 3 | 3 | 3 | 3 |
| 4 | 3 | 4 | 3 |
+---------------------+---------------+----------+---------+
Running each case:
call find_related_staff(2);
+--------+---------+
| result | role |
+--------+---------+
| 1,3,4 | Manager |
+--------+---------+
call find_related_staff(1);
+--------+------------+
| result | role |
+--------+------------+
| 3,4 | Supervisor |
+--------+------------+
call find_related_staff(3);
+--------+----------+
| result | role |
+--------+----------+
| 3 | Employee |
+--------+----------+
call find_related_staff(4);
+--------+----------+
| result | role |
+--------+----------+
| 4 | Employee |
+--------+----------+
Enjoy!
I think the most powerfull schema for hierarchical data in a relational database is the transitive-closure-table.
Given your sample data for the departments table:
department_id | parent_id | department_name
--------------|-----------|----------------
1 | 0 | TEST A
2 | 1 | TEST B
3 | 2 | TEST C
Your closure table (let's just call it departments_tree) would be like:
super_id | sub_id
---------|-------
1 | 1
1 | 2
1 | 3
2 | 2
2 | 3
3 | 3
Read it as: super_id = superordinate department_id; sub_id = subordinate department_id.
Assuming the logged-in user is manager of department with department_id = 2, the query to get all "supervised" employees is:
SELECT DISTINCT s.*
FROM departments_tree t
JOIN stuff_departments sd ON sd.department_id = t.sub_id
JOIN staff s ON s.id = sd.staff_id
WHERE t.super_id = 2
You can use triggers to populate and update the closure table.
Insert trigger:
DELIMITER //
CREATE TRIGGER `departments_after_insert` AFTER INSERT ON `departments` FOR EACH ROW BEGIN
INSERT INTO departments_tree (super_id, sub_id)
SELECT new.department_id, new.department_id
UNION ALL
SELECT super_id, new.department_id
FROM departments_tree
WHERE sub_id = new.parent_id;
END//
DELIMITER ;
Delete trigger:
DELIMITER //
CREATE TRIGGER `departments_before_delete` BEFORE DELETE ON `departments` FOR EACH ROW BEGIN
DELETE FROM departments_tree
WHERE sub_id = old.department_id;
END//
DELIMITER ;
Update trigger:
DELIMITER //
CREATE TRIGGER `departments_before_update` BEFORE UPDATE ON `departments` FOR EACH ROW BEGIN
DELETE t
FROM departments_tree p
CROSS JOIN departments_tree c
INNER JOIN departments_tree t
ON t.super_id = p.super_id
AND t.sub_id = c.sub_id
WHERE p.sub_id = old.parent_id
AND c.super_id = new.department_id;
INSERT INTO departments_tree (super_id, sub_id)
SELECT p.super_id, c.sub_id
FROM departments_tree p
CROSS JOIN departments_tree c
WHERE p.sub_id = new.parent_id
AND c.super_id = new.department_id;
END//
Note
You will not need a delete trigger, if you use foreighn keys with ON DELETE CASCADE:
CREATE TABLE `departments_tree` (
`super_id` INT(10) UNSIGNED NOT NULL,
`sub_id` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`super_id`, `sub_id`),
INDEX `sub_id_super_id` (`sub_id`, `super_id`),
FOREIGN KEY (`super_id`) REFERENCES `departments` (`department_id`) ON DELETE CASCADE,
FOREIGN KEY (`sub_id`) REFERENCES `departments` (`department_id`) ON DELETE CASCADE
);
Note 2
In many implementations of a transitive closure table, you will find a depth or level column.
But you don't need it for the given requirements. And I believe you will never really need it,
as long as you don't try to format tree output in SQL.
I think your main problem is how to traverse down so that will be solved by getRecursiveDepts(). I have not completed the code but you can try something like this
file db.php
class DB {
private $servername = "127.0.0.1";
private $username = "root";
private $password = "root";
private $dbname = "test";
private $port = '3306';
public function getRecursiveDepts($deptIds) {
if (!is_array($deptIds)) {
$deptIds = array($deptIds);
}
$sql = "SELECT id FROM Departments WHERE parentId IN (";
$sql .= implode(', ', $deptIds);
$sql .= ")";
$conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname, $this->port);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$newDept = array();
while($row = $result->fetch_assoc()) {
array_push($newDept, $row['id']);
}
$conn->close();
$moreDepts = $this->getRecursiveDepts($newDept);
if (is_null($moreDepts)) {
$finalIds = array_unique(array_merge($deptIds, $newDept));
} else {
$finalIds = array_unique(array_merge($deptIds, $newDept, $moreDepts));
}
return $finalIds;
} else {
$conn->close();
return null;
}
}
public function getRoles($empId) {
$sql = "SELECT role_id, department_id FROM staff_departmen_role WHERE staff_id = '$empId' GROUP BY role_id, department_id";
$conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname, $this->port);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$emp = array();
while($row = $result->fetch_assoc()) {
if (!array_key_exists($row['role_id'], $emp)) {
$emp[$row['role_id']] = array();
}
array_push($emp[$row['role_id']], $row['department_id']);
}
}
$conn->close();
return $emp;
}
public function getEmpDetails($empId) {
$sql = "SELECT role_id, department_id FROM staff_departmen_role WHERE staff_id = '$empId' GROUP BY role_id, department_id";
$conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname, $this->port);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$emp = array();
while($row = $result->fetch_assoc()) {
if (!array_key_exists($row['role_id'], $emp)) {
$emp[$row['role_id']] = array();
}
array_push($emp[$row['role_id']], $row['department_id']);
}
}
$conn->close();
return $emp;
}
}
file index.php
<?php
include_once 'db.php';
$objDB = new DB();
$empId = 2;
$emps = $objDB->getRoles($empId);
foreach ($emps as $roleId => $deptIds) {
switch ($roleId) {
case 1:
$allDeptIds = $objDB->getRecursiveDepts($deptIds);
break;
case 2://Supervisor GetEmpIds of current dept role >= 2
break;
case 3://Employee GetEmpIds of current dept role >= 2
$emp = $objDB->getEmpDetails($empId);
break;
default:
# code...
break;
}
}
$data = $objDB->getRecursiveDepts($empId);
print_r($data);
?>
I have 2 tables - products and productimages.
product has unique id and title.
productimages has productid and imageurl. these are examples of my tables:
products:
|id|title |
_____________
|1 |Laptop |
|2 |Speakers |
productimages:
|productid|imageurl|
___________________
| 1 |lap1.png|
| 1 |lap2.png|
| 1 |lap3.png|
| 2 |spe1.png|
Right now I have a nested loop in PHP.
loop through all rows of -> select * from products
and for every product inside the loop -> select * from productimages where productid = id which is basically another loop inside the first loop.
and then I take all productimages into array and decode to JSON [title,photos].
Now imagine you have 2 million rows in productimages, the query times are too high, is there any way to make it more efficient?
$query = "SELECT * FROM products ORDER BY id LIMIT 10;
$result = mysqli_query($con,$query);
if(mysqli_num_rows($result)>0)
{
$response = array();
while($row = mysqli_fetch_assoc($result)) {
$photos = array();
$id = $row["id"];
$title = $row["title"];
$queryp = "select imageurl from productimages where productid= '".$id."';";
$resultp = mysqli_query($con,$queryp);
if(mysqli_num_rows($resultp)>0)
{
while($row2 = mysqli_fetch_assoc($resultp)) {
$photourl = $row2["imageurl"];
array_push($photos,$photourl);
}
}
}
}
Some betterment for you could be:
1) Don't use select *. Use column names instead. e.g. select products.id, products.title, productimages.imageurl
2) Use JOIN instead of nested loop
So, you can try querying data like:
select products.id, products.title, productimages.imageurl
from products
join productimages on products.id = productimages.productid
ORDER BY products.id LIMIT 10
This case is not uncommon - you have two tables in a one to many relationship.
You should never nest an SQL call in a loop if you can possibly avoid it but there is a decision to be made about one SQL call or two.
A single SQL call could be:
SELECT id, title, imageURL
FROM products LEFT JOIN productImages ON id=productid
The disadvantage of this is that you are extracting the title several times for each product and this is wasteful.
Using two SQL statements you can download the titles once for each product:
SELECT id, title FROM products
The results of this query can be stored in an associative array - so that you can look up the title for each id.
The second query is:
SELECT productid, imageURL FROM productImages ORDER BY productid, imageURL
You can loop through the results of this query, spitting out the title as you go.
To save the images with product you can add a column imageurl in the products table.collect the image names with , and insert that image name string to the products table.
your table looks like below.
+--------------+--------------+---------------------------+
| id | title | imageurl |
+--------------+--------------+---------------------------+
| 1 | Laptop | lap1.png,lap2.png,lap3.png|
+--------------+--------------+---------------------------+
| 2 | Speakers | spe1.png |
Hope you understood what i explain.
I have two tables:
1 - hotels[id,name,extras] ( name of hotels with column extras which I've select for each one)
2 - extras[id,name] ( here's the extras of hotel like wifi,tv,swim... )
$name = $_GET['name'];
$hotels_q = mysql_query("SELECT * FROM `hotels` WHERE `name`='$name'") or die (mysql_error());
$hotels_row = mysql_fetch_array($hotels_q);
$id = $hotels_row['id'];
$extras = explode(",", $hotels_row['extras']);
$ekstras_q = mysql_query("SELECT * FROM `extras` order by id") or die(mysql_error());
While($ekstras_row = mysql_fetch_array($ekstri_q)){
$eid = $ekstras_row['id'];
$ename = $ekstri_row ['name'];
echo '<ul><li><input type="checkbox" name="extras['.$eid.'][]" value="'.$ename.'"';
if (in_array($eid, $ekstras)) echo'checked';
echo'/>'.$ename.'</li></ul>';
Problem is here extras_q displays all entries with checked ones from table, but I want only to display only checked items!
Since you're storing your extra IDs in one column as a comma separated string, I think you should be able to do this by not exploding that value, and then using the CSV string as an IN criteria in your second query.
//...
$extras = $hotels_row['extras']; // don't explode
// Use IN with the $extras CSV here
$ekstras_q = mysql_query("SELECT * FROM `extras`
WHERE id IN ($extras) ORDER BY id") or die(mysql_error());
If you are able to modify your database, you can instead create a many-to-many relationship between hotels and extras by adding a table to join those two items together instead of using a CSV column as you currently are. This can make it easier to write queries to select the related records.
If you add a third table hotel_extras with columns hotel_id and extra_id, you can insert one row for each extra that each hotel has. For example, if the hotel with id 1 has several different extras, its entries in that table would look like this:
table: hotel_extras
_______________________
| hotel_id | extra_id |
=======================
| 1 | 1 |
| 1 | 3 |
| 1 | 4 |
-----------------------
Here is an example using PDO of how you could query data from a setup like that:
$sql = "SELECT e.id, e.name
FROM hotels h
INNER JOIN hotel_extras he ON h.id = he.hotel_id
INNER JOIN extras e ON he.extra_id = e.id
WHERE h.`name` = ?";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(1, $_GET['name']);
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$eid = $row['id'];
$ename = $row['name'];
'<li><input type="checkbox" name="extras['.$eid.'][]" value="'.$ename.'" checked/>'.$ename.'</li>';
}
I have 3 columns in table1: book, key and value.
| book | key | value |
------------------------------
| 1 | author | a |
| 1 | editor | b |
| 1 | book | c |
Instead of runnuing three queries
$data = mysql_query("
SELECT * FROM table1 WHERE book = '1' AND key = 'author'
") or die(mysql_error());
while($info = mysql_fetch_array( $data ))
{
$value1 = $info['value'];
}
Then repeat this for editor and book.
$value1 $value2 $value3 are inserted in different places on page
Could I do this with one query?
Yes. If there are no other entries with "book = 1" you just query
SELECT * FROM table1 WHERE book = '1'
If there are more entries you can use this query:
SELECT * FROM table1 WHERE book = '1' AND key IN('author','editor','book')
And then create an assoc array:
while($info = mysqli_fetch_assoc( $data ))
{
$value[$info['key']] = $info['value'];
}
...
echo "the book {$value['book']} was written by {$value['author']}";
for create a $value array you have to use this :
$value[] = $info['value'];
instead of this :
$value1 = $info['value'];
Also you can use this code :
$value[]["key"] = $info['key'];
$value[]["value"] = $info['value'];
And for example you can call first row's value with $value[0]["value"]
If you want the values in a single record try this:
SELECT `t1`.`value` AS `value1`, `t2`.`value` AS `value2`, `t3`.`value` AS `value3`
FROM
`table1` AS `t1` CROSS JOIN
`table1` AS `t2` CROSS JOIN
`table1` AS `t3`
WHERE
(`t1`.`book` = 1) AND (`t1`.`key` = 'author')
AND (`t2`.`book` = 1) AND (`t2`.`key` = 'editor')
AND (`t3`.`book` = 1) AND (`t3`.`key` = 'book');