With only a bit of previous experience with databases and no formal education with them, I'm a bit stuck as to how to model this (and retrieve the data I require from it in PHP). This is what I'm trying to model:
For each item on my site, it is allowed to have multiple tags such as file, upload, php, recursive etc. However the tags are reusable, I can have two different items and they each could have the php tag.
I've been trying to read up on how to do this and whether it is lack of experience with it or something else I don't know, but I can't seem to grasp the concept. Apparently you need a middle table which links the two together?
Also once I have this relationship and the tables defined, how would I do things such as:
- Retrieve all items with a certain tag?
- Retrieve all tags that one item has?
Thanks for your help, also if anyone could list any further reading on this to strengthen my understanding of the concept that would be great.
The db part is easy. This is just a sample so you can see how db can look like, not any particular SQL engine queries.
CREATE TABLE posts (
id INT PRIMARY KEY,
subject VARCHAR(100),
body TEXT
)
CREATE TABLE tags (
id INT PRIMARY KEY,
name VARCHAR(50)
)
CREATE TABLE post_tags (
post_id INT,
tag_id INT,
FOREIGN KEY (post_id) REFERENCES posts (id),
FOREIGN KEY (tag_id) REFERENCES posts (id)
)
To get items with yourTag tag you will just run query like this
SELECT P.*
FROM posts P
LEFT JOIN post_tags PT ON (PT.post_id = P.id)
LEFT JOIN tags T ON (T.id = PT.tag_id)
WHERE T.name = 'yourTag';
To get tags associated with post with id of 123 you run this query:
SELECT T.*
FROM tags T
LEFT JOIN post_tags PT ON (T.id = PT.tag_id)
LEFT JOIN posts P ON (PT.post_id = P.id)
WHERE P.id = 123;
For the PHP part you could use a framework. Many (if not all) frameworks can easily model such relationships. For example in CakePHP this done like that:
class Post extends AppModel {
$useTable = 'posts';
$hasAndBelongsToMany = array(
'Tag' => array(
'className' => 'Tag'
'joinTable' => 'post_tags'
'foreignKey' => 'post_id'
'associationForeignKey' => 'tag_id'
)
);
}
class Tag extends AppModel {
$useTable = 'tags';
$hasAndBelongsToMany = array(
'Post' => array(
'className' => 'Post'
'joinTable' => 'post_tags'
'foreignKey' => 'tag_id'
'associationForeignKey' => 'post_id'
)
);
}
You should use an intermediate table, to relate the two entities:
-------- 1:n ------------ ---------
| ITEM |-¦---------<| ITEM_TAG | n:1 | TAG |
| Id | | ItemId |>-------¦-| Id |
| Name | | TagId | | Name |
¯¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯¯
Then for querying the data, you should join your tables in a select statement:
All the items in the tag "FooTag"
SELECT item.* FROM item
JOIN item_tag on item.id = item_tag.itemId
JOIN tag on item_tag.tagId = tag.id
WHERE tag.Name = 'FooTag'
All the tags for the item with name "FooItem"
SELECT tag.* FROM tag
JOIN item_tag on tag.id = item_tag.tagId
JOIN item on item_tag.itemId = item.id
WHERE item.Name = 'FooItem'
You're right, many-to-many relationships are implemented using additional table, for instance:
Blog_entry(entry_id, entry_body)
Tag(tag_id, tag_name)
Entry_tag(entry_id, tag_id)
Any operations are being done using multiple joins. For instance, if you want to select all entries with tag 'foo', using tables from my example, you have to execute:
select *
from
blog_entry, tag, entry_tag
where
tag.tag_name = 'foo' and
entry_tag.tag_id = tag.tag_id and
entry_tag.entry_id = blog_entry.entry_id
(update) To retrieve all tags that certain entry (here with ID 123) has:
select tag_name
from
blog_entry, tag, entry_tag
where
Blog_entry.entry_id = 123
entry_tag.tag_id = tag.tag_id and
entry_tag.entry_id = blog_entry.entry_id
Yeah, many-to-many relationship needs additional, third table, called association table.
Database part is not that hard, it's harder to use it in code with all those left joins and it can get pretty messy :)
My advice is to use ORM framework, like Doctrine or Propel (though I prefer Doctrine), which handle even some complex queries for you.
Related
I'm not sure why but I'm hitting an absolute wall trying to come up with this select statement. Maybe there is a PHP/MYSQL function that I'm not familiar with that would help. The idea is simple for this user management software: there are managers, and managers can (but do not have to) share clients. Amongst the manager and shared client relationship, one of the managers can be assigned as a lead. So here's how the basic example of what the database looks like for 1 client that is shared between 2 managers and assigned, and another client that is also shared but NOT assigned (represented by zero).
DROP TABLE IF EXISTS clients;
CREATE TABLE clients
(client_id SERIAL PRIMARY KEY
,client_name VARCHAR(12) UNIQUE
);
INSERT INTO clients VALUES
(555,'Jimmy'),
(789,'Tyler');
DROP TABLE IF EXISTS managers;
CREATE TABLE managers
(manager_id SERIAL PRIMARY KEY
,manager_name VARCHAR(12)UNIQUE
);
INSERT INTO managers VALUES
(123,'Michael'),
(456,'David');
DROP TABLE IF EXISTS relationships;
CREATE TABLE relationships
(client_id INT NOT NULL
,manager_id INT NOT NULL
,assigned INT NOT NULL
,PRIMARY KEY(client_id,manager_id)
);
INSERT INTO relationships VALUES
(555, 123, 0),
(555, 456, 1),
(789, 123, 0),
(789, 456, 0);
To get to the point: the statement I'm trying to make is for a manager to be shown all the clients that he has a relationship with, but are NOT assigned to him or anyone else on his team, i.e. select all of my clients where no one is assigned as the lead.
Expected input: Show all clients that manager 123 has a relationship with, but have yet to be assigned to any manager
Expected result: client 789
Happy to clarify as I can see this being overtly confusing as described.
SELECT c.*
FROM managers m
JOIN relationships r
ON r.manager_id = m.manager_id
JOIN clients c
ON c.client_id = r.client_id
LEFT
JOIN relationships x
ON x.client_id = c.client_id
AND x.assigned = 1
WHERE m.manager_id = 123
AND r.assigned = 0
AND x.client_id IS NULL;
+-----------+-------------+
| client_id | client_name |
+-----------+-------------+
| 789 | Tyler |
+-----------+-------------+
So you will have to start with finding all clientid's from the manager in RELATIONS, but remove all that have a manager assigned already.
Depending on the size of the table you might want to rewrite this, but here is one approach:
1) Get all clientids that have no manager:
SELECT R1.client_id, SUM(R1.assigned) as sumassigned FROM relationships AS R1 GROUP BY R1.client_id HAVING ( SUM(R1.assigned) = 0)
Now it is easier, you just join, eg:
SELECT R2.client_id, R2.manager_id FROM relationships AS R2
INNER JOIN
(SELECT R1.client_id, SUM(R1.assigned) as sumassigned FROM relationships AS R1 GROUP BY R1.client_id HAVING ( SUM(R1.assigned) = 0) ) AS DRVNOMANAGER
ON (R2.client_id = DRVNOMANAGER.client_id)
WHERE (R2.manager_id = 123)
Not tested. (meaning you might have to fix it)
The idea is to create a derived (temp) table containing all clients without a manager, then do an inner join on your original question ("what clients does this manager 123 know that do not have another assigned)
Does that solve your problem?
PS: Such things are much easier solved in PHP, but if your dataset is huge, that is not feasible.
OP asked for clientnames, so just add that:
SELECT R2.client_id, R2.manager_id, C.client_name FROM relationships AS R2
INNER JOIN
(SELECT R1.client_id, SUM(R1.assigned) as sumassigned FROM relationships
AS R1 GROUP BY R1.client_id HAVING ( SUM(R1.assigned) = 0) ) AS DRVNOMANAGER
ON (R2.client_id = DRVNOMANAGER.client_id)
INNER JOIN clients AS C ON (C.client_id = R2.client_id)
WHERE (R2.manager_id = 123)
And last request: removed deleted relations:
Simply add a WHERE clause to the inner DRVNOMANAGER with your restriction on the rows that are used in the GROUP BY. eg:
SELECT R2.client_id, R2.manager_id, C.client_name FROM relationships AS R2
INNER JOIN
(SELECT R1.client_id, SUM(R1.assigned) as sumassigned FROM relationships
AS R1 WHERE (NOT(R1.deleted = 1) ) GROUP BY R1.client_id HAVING ( SUM(R1.assigned) = 0) ) AS DRVNOMANAGER
ON (R2.client_id = DRVNOMANAGER.client_id)
INNER JOIN clients AS C ON (C.client_id = R2.client_id)
WHERE (R2.manager_id = 123)
==============================================
THIS WAS MY OLD ANSWER. NOT RELEVANT ANYMORE.
"select all of my clients where no one is assigned as the lead."
If I read you well that means: Get all clientid from RELATIONS where some managerid is given, AND assigned=0. (assigned=0 meaning "no one is assigned as the lead.")
Is that correct?
Then you end up with something like this (for managerid 123):
SELECT R.clientid, C.clientname FROM RELATIONSHIPS AS R WHERE ( (R.managerid = 123) AND (R.assigned=0))
INNER JOIN CLIENTS AS C ON (C.clientid = R.clientid)
I removed the spaces in the columnnames because I hate spaces in columnnames.
I have 2 tables: Tags and Post_Tags_relationship
Tag table has 3 columns - ID(primary), Title and URL
Post_Tags_relationship table has 2 columns - Tag_ID AND Post_ID (primary is the combination of both)
There are a lot of similar tags title and url in the Tags table, I want to delete all replicated records and also modify the Post_Tags_relationship to update the deleted tag id with the existing one, and if this update will return duplicate id error then remove it.
So if Tag table has:
ID= 20, Title = News Section, URL = news-section
ID= 68, Title = News Section, URL = news-section
Post_Tags_relationship has:
Post_ID = 56, Tag_ID = 20
Post_ID = 80, Tag_ID = 20
Post_ID = 500, Tag_ID = 68
Post_ID = 584, Tag_ID = 20
Post_ID = 695, Tag_ID = 20
Post_ID = 695, Tag_ID = 68```
If we delete ID 20 from Tags table, the Post_Tags_relationship will look like:
Post_ID = 56, Tag_ID = 68
Post_ID = 80, Tag_ID = 68
Post_ID = 500, Tag_ID = 68
Post_ID = 584, Tag_ID = 68
Post_ID = 695, Tag_ID = 68 // deplicate Primary key I want this to be removed please.
Post_ID = 695, Tag_ID = 68 // ```
I hope this makes sense, please let me know if you will have any questions.
Find tag duplicates and store them in a "temporary" table:
drop table if exists tmp_tags_duplicates;
create table tmp_tags_duplicates
select t1.id, min(t0.id) as duplicate_of
from tags t1
join tags t0 using(title, url)
where t1.id > t0.id
group by t1.id;
Find already inserted duplicates in posts_tags table (which need to be deleted). Store them in another "temporary" table:
drop table if exists tmp_to_delete;
create table tmp_to_delete
select pt1.*, d.duplicate_of
from posts_tags pt1
join tmp_tags_duplicates d on d.id = pt1.tag_id
join posts_tags pt0
on pt0.post_id = pt1.post_id
and pt0.tag_id = d.duplicate_of;
Find entries in posts_tags which need to be updated. Store them in a third "temporary" table:
drop table if exists tmp_to_update;
create table tmp_to_update
select pt1.*, d.duplicate_of
from posts_tags pt1
join tmp_tags_duplicates d on d.id = pt1.tag_id
left join posts_tags pt0
on pt0.post_id = pt1.post_id
and pt0.tag_id = d.duplicate_of
where pt0.tag_id is null;
Delete duplicates in posts_tags:
delete pt
from posts_tags pt
join tmp_to_delete t using(post_id, tag_id);
Update tag_id in posts_tags:
update posts_tags pt
join tmp_to_update t using(post_id, tag_id)
set pt.tag_id = t.duplicate_of;
Delete duplicates in tagstable
delete t
from tags t
join tmp_tags_duplicates using(id);
Delete the "temporary" tables.
drop table tmp_tags_duplicates;
drop table tmp_to_delete;
drop table tmp_to_update;
Demo: http://rextester.com/FUWZG89399
Now define proper UNIQUE and FOREIGN keys, so you won't need to fix it ever again.
I will give you an outline how I would approach this problem, I will assume your table isn't large and the queries aren't expensive:
Select all distinct titles from Tag Table, this can be done using the DISTINCT keyword. This will give the titles without replication:
SELECT DISTINCT Title FROM Tag
Loop the resulting titles and make a new query to the tags table for each title to get all duplicated fields for this specific title. You will end up getting rows with the same title.
Loop the rows with the same title replacing each one of them with the ID you want to keep and in the same time replacing this ID in the Post_Tags_relationship. All done with UPDATE statement
To avoid problems like this in the future use foreign keys. https://www.w3schools.com/sql/sql_foreignkey.asp
Update:
To avoid the error that will occur because of the duplicate primary key you can create an array and in the loop add to it every post id and if the post id already exists, delete this record from the table. Something like this:
$post_ids = array()
//...
// Duplicate fields loop
if ( in_array( $pid, $post_ids ) ) {
// The post has the tag already
// Delete this record from table
// ..
} else {
$post_ids[] = $pid
// Update fields
// ..
}
i have this table structure:
id
name
email
child_id <- pointing to id from "child" table
how SELECT this table data and propagate select to all childs with a SINGLE QUERY?
If you assume you're working with a child table with its own name and id field, you could write...
select c.name as child_name, p.name as parent_name
from child c
join parent p
on c.id = p.child_id
I made a SQLFiddle with the following SQL:
CREATE TABLE "parent" (
"id" INTEGER PRIMARY KEY,
"name" TEXT,
"email" TEXT,
"child_id" INTEGER
);
CREATE TABLE "child" (
"id" INTEGER PRIMARY KEY,
"name" TEXT
);
INSERT INTO parent
(id, name, email, child_id )
VALUES
(1, "Jim", "pineapple#fruit.com", 1 ),
(2, "Jane", "gizmo#gadget.com", 1 ),
(3, "Grover", "monty#python.com", 2 );
INSERT INTO child
(id, name)
VALUES
(1, "Ben" ),
(2, "Samantha" ),
(3, "Carl" );
My select query then gave me:
However, this data structure limits you to only on child per parent though I showed 2 parents for one of the kids. If you want multiple of either without having to duplicate the parent entries, you'd want to have a third table for the parent child relationship. That Table would have a parent_id, a child_id, and there are different schools of thought on whether it having it's own id would be required (it makes selecting specific rows in the relationship table easy and becomes more important if the relationship gains additional fields itself). Your query would need to join all the tables to the relationship table and if you did this, that part might look something like this:
from relationship r
join parent p
on p.relation_id = r.parent_id
join child c
on c.relation_id = r.child_id
New to PHP/MySQL and building a CMS. I need to know how to associated an Article with multiple Categories eg "My Article 1" has to be in 3 categories and "My Article 2" has to be in only 2 etc.
It's basically just the table set up to begin with. Here's what I have:
Table = articles
Fields = id, publicationDate, title, summary, content
Table = categories
Fields = id, categoryTitle
I know a little about joins and relationships etc but I want to get this right from the start.
You need a many-to-many table that will link article ids to category ids
CREATE TABLE articles_categories (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
articles_id INT,
category_id INT
) TYPE=myisam;
So for each category that you associate with an article you will need to insert a row into this table.
Perhaps something like:
$currentArticle = array('id' => 99, 'name' => 'Test Article');
$currentCategory = array('id' => 1, 'name' => 'First Category');
mysql_query(sprintf('INSERT INTO articles_categories (articles_id, category_id) VALUES (%d, %d)', $currentArticle['id'], $currentCategory['id']));
You need a third table, called articles_to_categories or whatever, with two fields:
article_id
category_id
Add an entry for each article and category association. Set the PK to be both columns.
I'm retrieving my data for part of my site with a typical MySQL query and echoing out the results from various fields etc etc from my main table whose structure is not important but which has a unique id which is 'job_id'
In order to have multiple catagories associated with that 'job_id' i have employed a toxi solution which associates catgories to each 'job_id'.
TABLE `tags` (
`tag_id` INT NOT NULL AUTO_INCREMENT,
`tag_name` VARCHAR(20) NOT NULL,
PRIMARY KEY (`tag_id`)
)
CREATE TABLE `tag_relational` (
`job_id` INT NOT NULL,
`tag_id` INT NOT NULL
)
What i want to do is, when i echo out the info from the main table (using 'job_id') i also want to echo all the catagories which that job_id is matched against.
The query below only returns the first catagory(tag_name) that the job_id is listed against, when it should be up to six (at the moment):
$query = "SELECT * FROM tags t
JOIN tag_relational r
ON t.tag_id=r.tag_id
WHERE r.job_id = $job_id";
$result=mysql_query($query) or die(mysql_error());
$cats=mysql_fetch_assoc($result);
In my code i'm using this to echo out the matched catagories:
<?php echo $cats['tag_name'];?>
Can someone explain how i can get ALL the catagory names to echo out rather than just the first?
Thanks
Dan
BTW, apologies to mu is too short who kindly answered my question when i had dummy/less complete information above.
If you just want to list the category names, then you could use group_concat sort of like this:
select b.*,
group_concat(c.category_name order by c.category_name separator ' ,') as cats
from business b
join tbl_works_categories w on b.id = w.bus_id
join categories c on w.category_id = c.category_name
where ...
group by b.id
You'd need a proper WHERE clause of course. That will give you the usual stuff from business and the category names as a comma delimited list in cats.
If you need the category IDs as well, then two queries might be better: one to get the business information and a second to collect the categories:
select w.bus_id, c.category_id, c.category_name
from tbl_works_categories w
join categories c
where w.bus_id IN (X)
where X is a comma delimited list of business ID values. Then you'd patch things up on the client side.