PDO - get from one to many structure - php

I have this db structure:
create table article(
id int AUTO_INCREMENT PRIMARY KEY,
title varchar(50),
text text
)
create table comments(
id int AUTO_INCREMENT PRIMARY KEY,
article int not null
username varchar(30) not null,
text text not null,
foreign key(article) references article(id) on delete cascade
)
I would like to get articles with comments and convert to json with this structure:
[
{
id: 1,
title: "article1",
text: "text1",
"comments": [
{
id: 1,
username: "user1",
text: "text"
}
]
}
]
This is my code:
$query = $pdo->query('select * from article as a join comments as c on c.article =a.id');
$query->execute();
var_dump(json_encode($query->fetchAll(PDO::FETCH_ASSOC)));
and result:
[{"id":"1","title":"artile1","text":"comment1","article":"1","username":"user1"}]
It is any way how to get article and comments as inner array? I could do it manually but, I will have a lot of tables with many columns.
Thanks for advices

It looks like it is not possible using PDO fetch modes. They are powerful, but unfortunately, I was not able to get the output you wanted.
You can achieve this outcome using a simple loop. The downside is that you have to create the array manually.
$stmt = $pdo->prepare('SELECT a.id AS aid, a.title, a.text AS atext, c.id AS cid, c.username, c.text AS ctext
FROM article AS a
JOIN comments AS c ON c.article =a.id ');
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_ASSOC);
$id = null;
$data = [];
foreach ($stmt as $row) {
$comment = [
'id' => $row['cid'],
'username' => $row['username'],
'text' => $row['ctext'],
];
if ($id == $row['aid']) {
// If parent ID still the same append only comment
$data[array_key_last($data)]['comments'][] = $comment;
} else {
// set new id and append a whole new row
$id = $row['aid'];
$data[] = [
'id' => $row['aid'],
'title' => $row['title'],
'text' => $row['atext'],
'comments' => [$comment]
];
}
}
PDO has plenty of fetch modes and you can mix them together, but it looks like none of them can cope with joins the way you would like them too. They are all described here in https://phpdelusions.net/pdo/fetch_modes

Related

SQL bind two tables into one SQL statement in a specific way

I have two SQL tables with columns:
menu:
id [AI]
name
description
subcategories:
id [AI]
name
pid (subcategory parent id)
mid (menu id)
In the new version of my website there is no menu anymore and "subcategories" are now changed into "categories":
id
name
description
parent_id
The old database is still in use, so I'm making my migration script and here is the part with categories:
$new_db->query("TRUNCATE TABLE `categories`");
$ids = [];
$menu_list = $old_db->fetch("SELECT * FROM `menu` ORDER BY `id`");
foreach($menu_list as $menu)
{
$id = $new_db->insert("categories", [
"name" => $menu["name"],
"description" => $menu["description"],
"parent_id" => "0"
]);
$ids[$menu["id"]] = $id;
}
$subcategories = $old_db->fetch("SELECT * FROM `subcategories` ORDER BY `id`");
foreach($subcategories as $subcategory)
{
$pid = 0;
$desc = "";
if($subcategory["mid"] > 0)
{
$menu = $old_db->fetch_first("SELECT `id`, `description` FROM `menu` WHERE `id` = '".$subcategory["mid"]."' LIMIT 1");
$pid = $ids[$menu["id"]];
$desc = $menu["description"];
}
else
{
$pid = $subcategory["pid"];
}
$new_db->insert("categories", [
"name" => $subcategory["name"],
"description" => $desc,
"parent_id" => $pid
]);
}
It works but I'm pretty sure it could be done better with lesser cost.
Can I make a SQL statement which will bind menu and subcategories into one result list and then insert them all?
I think you just want a JOIN?
SELECT *
FROM `categories` c
JOIN `subcategiries` s
ON s.pid = c.id

Echoing Sorted Multidimensional Array

Ok, so I am creating a web app with php and mysqli.
I have a table friends which is a simple set up:
f_id int(11)
uid int(11)
fids TEXT
now its basically like a row for each user with the fids consisting of a lot of numerical values (other userids) separated by commas like: 1,2,3
so I use this function to get each user's friends:
function getFriends($db, $userid)
{
$q = $db->query("SELECT fids FROM friends WHERE uid='$userid'");
$ar = $q->fetch_assoc();
$friends = $ar['fids'];
$fr = explode(",", $friends);
return $fr;
}
but each posts comments that appear to each of their friends. my problem comes from trying to sort these comments by the time they were posted.
lets say my comments table is:
c_id int(11)
uid int(11)
c_text TEXT
c_time int(11)
I want to be able to get the comments posted by each 'friend' put them all into an array together, then sort them from their c_time value, then all the values from that particular row in the comments table.
The problem comes from my how I've set up my friends table.
I'm using:
$fr = getFriends($db, $userid);
$updates = array();
$i = 0;
foreach( $fr as $friend)
{
// Get Updates from friends and from self
$q = $db->query("SELECT up.*, u.* FROM updates up
LEFT JOIN users u ON u.id = '$friend'
WHERE (up.userid = '$userid') ORDER BY up.up_id DESC");
while($ar = $q->fetch_array(MYSQLI_BOTH))
{
$updates[$i] = $ar;
$i++;
}
}
$sortArray = array();
foreach($updates as $update){
foreach($update as $key=>$value){
if(!isset($sortArray[$key])){
$sortArray[$key] = array();
}
$sortArray[$key][] = $value;
}
}
$orderby = "up_id";
array_multisort($sortArray[$orderby],SORT_DESC,$updates);
$updates_limit = array_slice($updates, 0, 20);
to get the comments from each friend, sorting it by time, then slicing it to the first 20.
However when I var_dump($updates_limit) it takes the last row in the comments table, and then makes it look like each friend posted the same comment.
Can anyone see the problem or a better way of addressing this issue?
I'd completely refactor the friends table to look something more like this: (Also, use english - Characters are cheap :c))
CREATE TABLE friends (
user_id int FOREIGN KEY REFERENCES user(id)
, friend_id int FOREIGN KEY REFERENCES user(id)
, PRIMARY KEY (user_id, friend_id)
);
Then you can take essentially the same comment table:
CREATE TABLE comment (
comment_id int PRIMARY KEY
, user_id int FOREIGN KEY REFERENCES user(id)
, comment_text text
, comment_time datetime
);
And your "query for friend's comments" becomes:
SELECT comment_id, comment.user_id, comment_text, comment_time
FROM friends
INNER JOIN comment
ON comment.user_id = friends.friend_id
WHERE friends.user_id = ? #Target it
ORDER BY comment_time DESC
LIMIT 0, 20;
You can even speed this up by adding a few indexes - like comment(user_id).

In MySQL how best to get a recordset with multiple children

Say I have a table that represents events, with a primary event_ID key. Data might be event_name, event_location, event_date.
Then I have a table that represents attendees, with a primary attendee_ID key. Data might be first_name, last_name, contact_number
I have a link table which uses the event_ID and attendee_ID as foreign keys.
Now I need to query this. I want to return a record set of events between two given dates. I also need to look up the attendees of each event.
I'll be using PHP to map these records to an object. What I'll be expecting is something like:
array(
// Multiple `events`
array(
'event_ID' => 1,
'event_location' => 'London',
'event_start' => '2014-11-27',
'attendees' => array(
// multiple `attendees`
array(
'attendee_ID' => 1,
'first_name' => 'John'
'last_name' => 'Smith',
'contact_number' => '0207 123 1234',
),
...
),
),
...
);
I have tried Googling this, but cannot think of how to express what I need to achieve in a search term. I guess I need to Query heirachical data.
What I don't want to do is get a record set of events, then iterate through them making more DB calls to populate the attendees, as this seems horribly inefficient.
Another idea I was think of was to get all event records, then query for the link table, collate the attendee IDs and query the attendee table for any that match the meetings and use PHP code to re-associate them with my event object I built from the recordset.
Is there a more elegant, best practice way of achieving this?
select
event . *,
CONCAT('[',
GROUP_CONCAT(CONCAT('{"name":"',
attendees.name,
'", id:"',
attendees.id,
'"}')),
']') attendees
FROM
`event`
LEFT JOIN
attendees ON attendees.event = event.id
GROUP BY event.id
Will return your results in the following format
1 event2 [{"name":"name-1-1", id:"6142"},{"name":"name-1-1", id:"2048"},{"name":"name-1-1", id:"1"},{"name":"name-1-1", id:"4095"}]
2 event3 [{"name":"name-2-2", id:"2"},{"name":"name-2-2", id:"4096"},{"name":"name-2-2", id:"6143"},{"name":"name-2-2", id:"2049"}]
3 event4 [{"name":"name-3-3", id:"6144"},{"name":"name-3-3", id:"2050"},{"name":"name-3-3", id:"3"},{"name":"name-3-3", id:"4097"}]
4 event5 [{"name":"name-4-4", id:"4"},{"name":"name-4-4", id:"4098"},{"name":"name-4-4", id:"6145"},{"name":"name-4-4", id:"2051"}]
5 event6 [{"name":"name-5-5", id:"6146"},{"name":"name-5-5", id:"2052"},{"name":"name-5-5", id:"5"},{"name":"name-5-5", id:"4099"}]
6 event7 [{"name":"name-6-6", id:"6"},{"name":"name-6-6", id:"4100"},{"name":"name-6-6", id:"6147"},{"name":"name-6-6", id:"2053"}]
The attendees result is a valid JSON and can be decoded using JSON_DECODE.
Make sure you have indexes This works in 0.047 seconds on 2000+ events and 8000+ attendees
I assume that you have a column on attendees table for specifying which event user attended. This is called attended_event_ID. By using this, you can do that with following;
<?php
$query = "SELECT e.event_ID, e.event_location, e.event_start,
a.attendee_ID, a.first_name, a.last_name, a.contact_number, a.attended_event_ID
FROM events e
LEFT JOIN attendees a ON a.attended_event_ID = e.event_ID
ORDER BY e.event_ID";
$result = $db->query($query);
while($row = $result->fetch_assoc()){
$events[$row["event_ID"]]["event_ID"] = $row["event_ID"];
$events[$row["event_ID"]]["event_location"] = $row["event_location"];
$events[$row["event_ID"]]["event_start"] = $row["event_start"];
$events[$row["event_ID"]]["attendees"][] = array(
"attendee_ID" => $row["attendee_ID"],
"first_name" => $row["first_name"],
"last_name" => $row["last_name"],
"contact_number" => $row["contact_number"]
);
}
?>

Multiple SQL queries and FOREACH - Can be JOINED to one?

I am trying to pull a list of Events, also seeing which members have paid for the Events. I then want to see if they are on the committee, to see if they have admin permissions.
I have successfully done this, using three SQL queries, then using three foreach loops to build the Array.
I am SURE this can be done with one SQL query and one foreach loop, however I have not yet mastered the JOIN technique.
I am using Expression Engine, Codeigniter Active Record, I will display to you the SQL output and also what my current EE functions look like.
THANKS FOR THE HELP! :D
SQL to select ALL events which are active
SELECT `id` as event_ID, `name` as event_name, `description` as event_description
FROM (`events`)
WHERE `events_category_id` = '1'
AND `active` = 1
ORDER BY `name` asc
EE CODE to achieve this:
$query = $this->theDb->select('id as event_ID, name as event_name, description as event_description')
->order_by("name", "asc")
->get_where('events', array('event_category_id'=>$event_type,'active'=>1));
**
SQL to find what EVENT IDs the user has paid for
**
SELECT DISTINCT `products`.`event_ID` as joinedID
FROM (`transactions_items`)
JOIN `transactions` ON `transactions`.`id` = `transactions_items`.`id`
JOIN `products` ON `products`.`id` = `transactions_items`.`product_id`
JOIN `events` ON `events`.`id` = `products`.`event_ID`
WHERE `transactions`.`member_id` = 27500
AND `events`.`active` = 1
AND `event_category_id` = '1'
ORDER BY `events`.`name` asc
EE CODE to achieve this
$query = $this->theDb->select('products.event_ID as joinedID')
->distinct()
->order_by("events.name", "asc")
->join('transactions', 'transactions.id = transactions_items.id')
->join('products', 'products.id = transactions_items.product_id')
->join('events', 'events.id = products.event_ID')
->get_where('transactions_items', array('transactions.member_id' => $memberID, 'events.active' => 1,'activity_category_id'=>$activity_type));
SQL to find ADMIN rights
SELECT `events`.`id` as event_ID, `admins`.`admin_role_id` as role_id, `admins_roles`.`name` as role_description
FROM (`admins`)
JOIN `admins_roles` ON `admins`.`admin_role_id` = `admins_roles`.`id`
JOIN `events` ON `events`.`id` = `admins`.`event_ID`
WHERE `admins`.`member_id` = 27500
AND `events`.`active` = 1
EE CODE to achieve this
$query = $this->theDb->select('events.id as event_ID, admins.admin_role_id as role_id, admins_roles.name as role_description')
->join('admins_roles', 'admins.admin_role_id = admins_roles.id')
->join('events', 'events.id = admins.event_ID')
->get_where('admins', array('admins.member_id' => $memberID, 'events.active' => 1));
FOR EACH LOOPS
// Create list of Events setting defaults
foreach($events_list as $row)
{
$combinedEvents[$row->event_ID] = array(
'eventID' => $row->event_ID,
'eventName' => $row->event_name,
'eventDescription' => $row->event_description,
'isJoined' => 0,
'roleID' => 0,
'roleDescription' => "",
);
}
// Add Committee roles
foreach($admin_list as $row)
{
$combinedEvents[$row->event_ID]['roleID'] = $row->role_id;
$combinedEvents[$row->event_ID]['roleDescription'] = $row->role_description;
}
// Add Transactions
foreach($transaction_list as $row)
{
$combinedEvents[$row->joinedID]['isJoined'] = 1;
}
I don't quite understand the FOREACH part because I've never touched PHP - but you should be able to solve the multiple SQL queires using the ;with clause. I have created an example in response to another question here and here. Is this what you're looking for?

mysql query to an array

I am new to php. I can't figure out how to query my db to list this mockup. For each array, I want to query the db to list only what is related to the left in ''.
My db consist of 2 tables.
table1 = album
id, name
table2 = picture
id, album, picPath
$q_albums = array(
'People' => array(
ALBUM_PATH.'4.jpg',
ALBUM_PATH.'11.jpg',
ALBUM_PATH.'10.jpg'),
'Nature' => array(
ALBUM_PATH.'2.jpg',
ALBUM_PATH.'3.jpg',
ALBUM_PATH.'5.jpg',
ALBUM_PATH.'6.jpg',
ALBUM_PATH.'7.jpg'),
'Art' => array(
ALBUM_PATH.'1.jpg',
ALBUM_PATH.'8.jpg',
ALBUM_PATH.'9.jpg',
ALBUM_PATH.'2.jpg'),
'Wilderness' => array(
ALBUM_PATH.'3.jpg',
ALBUM_PATH.'2.jpg',
ALBUM_PATH.'5.jpg',
ALBUM_PATH.'7.jpg',
ALBUM_PATH.'6.jpg'),
'Photography' => array(
ALBUM_PATH.'8.jpg',
ALBUM_PATH.'1.jpg',
ALBUM_PATH.'9.jpg',
ALBUM_PATH.'12.jpg'),
'Fashion' => array(
ALBUM_PATH.'11.jpg',
ALBUM_PATH.'4.jpg',
ALBUM_PATH.'10.jpg'),
);
As dev-null-dweller notes, you need to build the array from the query results in a loop. Fortunately, it's pretty easy:
$sql = <<<END
SELECT album.name AS albumName, picture.picPath AS picPath
FROM album JOIN picture ON album.id = picture.album
END;
$res = $mysqli->query( $sql );
$q_albums = array();
while ( $row = $res->fetch_object() ) {
// this line actually build the array, entry by entry:
$q_albums[ $row->albumName ][] = $row->picPath;
}
$res->close();
Edit: As Mr. Radical notes, if you want your results to include empty albums, you should change the JOIN to a LEFT JOIN. If you do that, the query will return a row with a null picPath for any empty albums, so you'll have to deal with those null paths somehow. One way would be to include something like the following code after building the array:
foreach ( $q_albums as $album => &$pics ) {
// remove dummy null entries from empty albums
if ( !isset( $pics[0] ) ) array_shift( $pics );
}
#Ilmari Karonen I would use LEFT JOIN instead of JOIN.
SELECT album.name AS albumName, picture.picPath AS picPath
FROM album LEFT JOIN picture ON album.id = picture.album

Categories