Okay i'm going to attempt to reword the question that was put on hold. I have a table in my database called anime this is what it looks like:
the code below mainly focuses on the tags row in the database which will has tags listed like this: tag1, tag2, tag3 etc..
I am trying to make a search that works via selecting keywords (tags) from a dropdown options menu (well 3 dropdown menus) since all the keywords (tags) are fixed and the tags within the database are fixed (the same) i can use
$tag_x = "tag1";
$tag_y = "";
$tag_z = "";
$tag_search = "%(?=.*$tag_x)(?=.*$tag_y)(?=.*$tag_z)%";
// Connect to Database
require 'connect.php';
$sql = "SELECT id, title, category FROM anime";
$result = $conn->query($sql);
if ($result->num_rows > 0)
{
// output data of each row
while ($row = $result->fetch_assoc())
{
$tags = $row["category"];
if (preg_match($tag_search, $tags)) {
echo $row["id"];
}
}
}
else
{
echo "0 results";
}
$conn->close();
this code can show matches for 1 - 3 keywords (tags)
the problem is i don't think this is a great way to do it and also i need to run it three times to get a list of results that have 3 matches first then 2 matches then 1 tag match, in a list.
////// OLD QUESTION BELOW THAT WAS PUT ON HOLD. (not sure if i should delete.) ///////////
Hello I am new to mysqli and databases in general I have been trying to find a way to filter down SELECT queries and display the results.
I have a database with lists of items that all have related tags and I am trying to show the row(s) and their information when a user makes a search query (via drop down menus). The data looks like this:
ID NAME TAGS CAT IMG_FILE_NAME
1 demo one tag1, tag2, tag3 ... something.png
2 demo two tag2, tag4 ... something.png
3 demo three tag0 ... something.png
4 demo four tag3, tag4, tag5 ... something.png
5 demo five ... ... something.png
6 ... ... ... ...
the search will work like this
<label for="x">I am looking for:</label>
<select id="x">
<option value="t1">Tag 1</option>
<option value="t2">Tag 2</option>
<option value="t3">Tag 3</option>
</select>
<label for="y">That has:</label>
<select id="y">
<option value="t4">Tag 4</option>
<option value="t5">Tag 5</option>
<option value="t6">Tag 6</option>
</select>
<label for="z">and is:</label>
<select id="z">
<option value="t7">Tag 7</option>
<option value="t8">Tag 8</option>
<option value="t9">Tag 9</option>
</select>
and the user may select x = tag 2 y = tag 5 z = tag 8
and then hit search or go and it would pull/show the results from the database with the tags the selected and failing that show the ones with the next closest matching tags (2 tags then 1 tag etc..)
can the WHERE statement do this?
I think 90% of my problem with finding an answer to this question is how i'm wording the question in the first place but after 3+ hours looking for something that works and not finding anything this seems like the best place to ask once again! any help would be amazing! and code/links to code would be greatly apperated!
The comma-separated values in your TAGS column are sub-optimal. You can, possibly, make this work like so
SELECT NAME, CAT, IMG_FILE_NAME
FROM item
WHERE TAGS LIKE CONCAT('%', ?tag1, '%')
AND TAGS LIKE CONCAT('%', ?tag2, '%')
AND TAGS LIKE CONCAT('%', ?tag3, '%')
where ?tag1 etc are the values provided by your user. But this sort of wildcard matching (WHERE TAGS LIKE '%tag3%', for example) is both error-prone and horribly slow if your table is large. (If your user gives you less than three tags, you need to use less than three WHERE clauses.
Do you want this to work well? Put the tags in their own tag table, with columns like so:
item_id
tag
So, if you item with id = 2 has tag2 and tag4, you'll have two rows in the tag table
2 tag2
2 tag4
Then, you can search with queries like this
SELECT COUNT(*) MATCHES, i.NAME, i.CAT, i.IMG_FILE_NAME
FROM tag t
JOIN item i ON t.item_id = i.id
WHERE t.tag = ?tag1
OR t.tag = ?tag2
OR t.tag = ?tag3
GROUP BY i.NAME, i.CAT, i.IMG_FILE_NAME
ORDER BY COUNT(*) DESC, i.NAME
This will give you a list of the items that match any or all the tags your user specified, with the ones that match more tags listed first.
yes, you should write something like:
SELECT field_a, field_b FROM table WHERE field_x='2' AND field_y='5' AND field_z='8'
it is obvious that you have to adapt it to your table and to your field names but can give you a hint.
Based on your comment you should do something like:
SELECT field_a, field_b FROM table WHERE tags='2, 5, 8'
but note that this will look for the exact string. So if you have a row where tag = '5,2,8' (same tags but different order) you will not get the result.
It is better (IMHO) to use three columns like tag_1, tag_2 and tag_3 and store each value in a single column? So that my first example will become
SELECT field_a, field_b FROM table WHERE tag_1='2' AND tag_2='5' AND tag_3='8'
Related
I've concatenated 2 fields in a form's drop down list, with the 2 fields being First Name and Last Name; This allowed me to display a person's full name.
The issue is that in the database a person's full name is stored in 2 seperate fields; First Name and Last Name.
I need to match the person's full name (from the dropdown list) to the First Name and Last Name in the DB, however I am struggling to achieve this.
The php code for the concatenated form list is:
<td>
<select name='IndivSurname'>";
while($FullName_row = odbc_fetch_array($sql_run_FullName)){
$IndivFirstName=$FullName_row['FirstName'];
$IndivLastName=$FullName_row['LastName'];
echo"<option value='$IndivIndivId' . '$IndivTenantNdx'> $IndivFirstName $IndivLastName</option>";
}
</select>
</td>
While the SQL statement is:
SELECT EventId, EventTime, Individual, Tenant, TenantName, DeviceName, Comment,
InetDb.dbo.Individuals.FirstName, InetDb.dbo.Individuals.LastName
FROM taclogdata.dbo.Event
LEFT JOIN InetDb.dbo.Tenants
ON taclogdata.dbo.Event.Tenant = InetDb.dbo.Tenants.TenantId
LEFT JOIN InetDb.dbo.Individuals
ON taclogdata.dbo.Event.Individual = InetDb.dbo.Individuals.IndivId
AND taclogdata.dbo.Event.Tenant = InetDb.dbo.Individuals.TenantNdx
WHERE (taclogdata.dbo.Event.EventTime BETWEEN '00:00:00 05/26/2015'
AND '09:00:00 05/26/2015'
AND (taclogdata.dbo.Event.Comment ='Reader entry'
OR taclogdata.dbo.Event.Comment='Reader exit')
AND (InetDb.dbo.Individuals.FirstName = '$IndivFirstName'
AND InetDb.dbo.Individuals.LastName = '$IndivLastName')";
Many thanks in advance
The easiest way to do this, is by splitting the string on the space. However, that introduces a bug if the person has more than one first- or surname. This means that in the cases where we have 3 or more elements in the result of the split, we'll have to add the middle elements to both matches; Seeing as we don't know whether it's a surname or middle name.
That is why I recommend using a different character for the glue in the value attribute, one which is never used in a name. # is one such character.
This leaves you with the following code for the form-generation:
// CF: Added HTML-escaping to prevent XSS-attacks.
$IndivFirstName=htmlspecialchars ($FullName_row['FirstName']);
$IndivLastName=htmlspecialchars ($FullName_row['LastName']);
echo"<option value='{$IndivFirstName}#{$IndivLastName}'> $IndivFirstName $IndivLastName</option>";
In the SQL-statement you can do the following (using prepared statements):
$stmt = $db->prepare ("SELECT EventId, EventTime, Individual, Tenant, TenantName, DeviceName, Comment, InetDb.dbo.Individuals.FirstName, InetDb.dbo.Individuals.LastName
FROM taclogdata.dbo.Event
LEFT JOIN InetDb.dbo.Tenants
ON taclogdata.dbo.Event.Tenant = InetDb.dbo.Tenants.TenantId
LEFT JOIN InetDb.dbo.Individuals
ON taclogdata.dbo.Event.Individual = InetDb.dbo.Individuals.IndivId
AND taclogdata.dbo.Event.Tenant = InetDb.dbo.Individuals.TenantNdx
WHERE (taclogdata.dbo.Event.EventTime BETWEEN '00:00:00 05/26/2015' AND '09:00:00 05/26/2015'
AND (taclogdata.dbo.Event.Comment ='Reader entry' OR taclogdata.dbo.Event.Comment='Reader exit')
AND (InetDb.dbo.Individuals.FirstName = :firstname AND InetDb.dbo.Individuals.LastName = :lastname)");
$name = explode ("#", $_POST['IndivSurname']);
$stmt->exec (array (":firstname" => $name[0], ":lastname" => $name[1]));
Splitting on spaces is not going to be reliable given the number of De Aches and Mc Pains in this world.
The really sure way of doing this is to save the first name and last name in a hidden table within your form (in the same order as your pulldown).
A possible alternative is to concatenate using a not a space but one of the other white space characters like NO BREAK SPACE "\u00A0" which you can then reliably split later.
Ok I solved my problem by POSTing the user's PK rather than his First Name and Last Name individually, and search using those characteristics.
However I introduced the explode function as per the below to separate the concatenated values as per the below:
SQL query selects the IndivId, TenntNdx, individual's first name and last name, however only the first name and last name of the user are displayed in the form's drop down list. The matching IndivId and TenantNdx are posted using the code below:
$sql_FullName = "SELECT IndivId, TenantNdx, FirstName, LastName FROM InetDb.dbo.Individuals
ORDER BY FirstName ASC";
$sql_run_FullName = odbc_exec($conn_general, $sql_FullName);
while($FullName_row = odbc_fetch_array($sql_run_FullName)){
$IndivIndivId = $FullName_row['IndivId'];
$IndivTenantNdx = $FullName_row['TenantNdx'];
$IndivFirstName=$FullName_row['FirstName'];
$IndivLastName=$FullName_row['LastName'];
echo"<option value='$IndivIndivId $IndivTenantNdx'> $IndivFirstName $IndivLastName</option>";
The code above would POST the IndivId and TenantNdx (which form a composite primary key for the individuals table) to the page containing the code below which would then separate the 2 fields and search by the PK rather than the individual's name:
$IndivSurname = $_POST['IndivSurname'];
$explode_sql = explode(" ", $IndivSurname, 2);
$ListIndivId = $explode_sql['0'];
$ListTenantId = $explode_sql['1'];
I have a table...
Table: forums
for_id for_name for_des for_mem
1 Forum 1 Description 1 mem1#email.com, mem2#email.com
2 Forum 2 Description 2 null
3 Forum 3 Description 3 mem1#email.com
Then i have another table: members
mem_id mem_name mem_email ...
1 Jane mem1#email.com
2 Jack mem2#email.com
3 Smith mem3#email.com
I am trying to create an HTML Options list. A multi select list that would display all columns in members and then select those in a forums when editing the information.
Example: Editing for_id = 1
It should display all users and select only users who are in for_id=1's for_mem
Currently i am using:
<? $data = explode(',',$row[for_mem]); ?>
<div class="col-md-9">
<select name="for_mem" id="" multiple class="form-control">
<?php foreach($data as $key => $value){?>
<option value="<?php echo $value; ?>"><?php echo $value; ?></option>
<?php } ?>
</select>
</div>
But it only displays the users in the for_mem for that Forum, not all users for them to select new ones to add. Also, just the email. Is there a way to display the name?
Please help. Thanks
This query can solve partly & then join both by union. first part is members that are exist in forums and second part members that are not exist for the forums.
I have added a field "checked" which is 1 for first part and 0 for second part used for identified which users to checked when display.
select for_id, for_name, for_mem, mem_name, mem_email, 1 checked from forums, members where locate(mem_email, for_mem) > 0
union
select for_id, for_name, for_mem, mem_name, mem_email, 0 checked from forums, members where locate(mem_email, for_mem) = 0;
may be there any syntactically mistake i haven't tested because of data unavailable. You can use group by or group concat for more. I advice to run it on mysql prompt first and see the result and modify as your need.
I have 3 MySQL tables:
blog_posts // Contains blog posts
blog_tags // Contains tags
rel__blog_post_tags // Relates tags to a blog post (refering relation IDs)
This is my PHP code:
// I get tags from URL: /tag/tag1;tag2;tag3
$tags = explode(';', $options['req_tags']);
// Prepare for prepared SQL query
$in = str_repeat('?,', count($tags) - 1) . '?';
// Prepare SQL param array as I need to mix category ID with tags...
$sql_params = $tags;
// This SQL will select with IN (equals to OR but want to change to AND)
$sql_get_posts = '
SELECT * FROM blog_posts
WHERE category_id = ?
AND post_id IN
(SELECT post_id FROM rel__blog_post_tags
WHERE tag_id IN (' . $in . '));';
// Prepare the query
$get_posts = $PDO->prepare($sql_get_posts);
// Add category ID to beginning of array of SQL params (in front of tags)
array_unshift($sql_params, $options['category_id']);
// Now execute and get blog posts to array
$get_posts->execute($sql_params);
$postdata = $get_posts->fetchAll();
?>
OK so my problem here is that I use "WHERE tag_id IN...". This will return any blog posts that matches:
...AND post_id IN (SELECT post_id FROM rel__blog_post_tags WHERE tag_id='tag1' OR tag_id='tag2' OR tag_id='tag3'... and so on.
So basically, I cannot narrow down my blog post results by adding more tags, which is what I am after.
How can I change this SQL query to make it so that it will be like the below, so that if I have a blog post with ID "foobar" and it has a connection with tag1 and tag2 in the database it will not show up, as I am looking for blog post with ID "foobar" that has tag tag1, tag2 AND tag3:
...AND post_id IN (SELECT post_id FROM rel__blog_post_tags WHERE tag_id='tag1' AND tag_id='tag2' AND tag_id='tag3'... And so on...
You can fix it by creating the set of "and tag_id=?" string using for loop.
Like shown below:
for($i=0; $i<count($tags);$++){
if($i=0){
$condition="tag_id=$tags[$i]";
}
else{
$condition.="and tag_id=$tags[$i]";
}
}
The reason this is not working is that the select statement is analyzing each row. A single row can only have one value for tag_id. When you are using AND you are checking to see if the tag_id for that row is equal to multiple values which can never be true.
In a real-world example, I cannot both be wearing shoes AND boots at the same time.
I wrote a blog post based on this question, http://www.kurungkurawal.com/2015/03/23/sql-filter-data-berdasarkan-beberapa-baris-data/ but it is in Bahasa Indonesia.
Basically (based on my example)
Collect the tid of desired tag
Get all the posts that contains the tag(s), as you use with SELECT ... WHERE IN (...) make sure it doesn't contain duplicate row, you can prevent it by using primary key on both field, or use distinct
Count occurance of each pid
filter by using having on pid that have number of occurance equal on number of tags we are filtering.
Sorry if my explanation is not crystal clear. Hope this helps.
My database structure is like this:
Movie Name GenreID**
Movie1 1,2,3
Movie2 2,4
Movie3 4,5,16
I need to select a Movie name based on the Genre ID the user selected which I put inside the genreIDArray[]
Let's say for example the genreIDArray has values: $genreIDArray = ['1','2','3'];
My current query method is the ff:
Here I prepared each ID into parts so the result won't become genreID LIKE (%1,2,3%) because I checked this doesn't work.
So I did this separation loop:
$queryParts = array();
foreach($genresIDArray as $genreID) {
$queryParts[] = "'%".$genreID."%'";
}
After the separation loop I put together the final query:
$genreString = implode(" OR genreID LIKE ",$queryParts);
$genreQuery = " SELECT * FROM movies WHERE (genreID LIKE {$genreString}) ";
gave me this final query output:
SELECT * FROM movies WHERE (genreID LIKE '%1%' OR genreID LIKE '%2%' OR genreID LIKE '%3%')
This actually works, but apparently not that efficient because genreID 11,12,13 and so on that start with 1 is also selected. I think I'm missing the MYSQL LIKE logic here. I've tried '%$genreID' which means to select the starting or first number/letter of a table data, but that's still the same thing, $genreID% doesn't and would not work because this only means genreid ENDING letters/number will be selected.
I hope I spelled that out clear enough. I'm in a bind here. Please help.
Thank you so much.
There is a very cool function for that. You can use FIND_IN_SET.
SELECT * FROM test WHERE FIND_IN_SET(1,colors)
But if its possible you should avoid such structures in your database and normalize your database.
I have three tables (I simplified it in this question) :
Table 1
id | Name
-----------
1 | John
2 | Smith
Table 2
id | Title
-----------
1 | Developer
2 | Web Developer
3 | A New Title
Table 3 (links between 1 and 2)
idName | idTitle
-----------
1 | 1
1 | 2
2 | 1
My problem is with the update page. My HTML is <select multiple> ... options ... </select> and I am using chosen to select and deselect options. I am trying to do this with PHP only.
Lets say that we have the following scenario:
The administrator wanted to remove the 'Developer' Title for 'John' and add 'New Title' for 'John'.
He/She will deselect Title id 1 and select id 3. When He/She submits the form I will get two select values: id 1 (the one that he selected) and id 2 (the one that was already there). But I will not get id 3 because he deselected it.
What I am struggling with is : when the user posted the new selected values the ones that were deselected were not submitted with the form. There is no way for me to track the ones that were deselected so I can delete them from my table. How do you update your table with the new changes then? Do you delete what is already existed in that table and add the ids again? Is there a better option?
UPDATE :
It seems #wander answer is less destructive than the other ones. However, #wonder did not explain how to compute step 4 in his answer. to distinguesh between new selected options, already existing selected options, and deselected options.
There's no need to submit the deselected one. e.g.
John has two titles(Developer and Web Developer) saved in database.
The administrator opens the page, selects 'New Title', deselects 'Developer' and clicks submit.
Now on server side, we can get
the title list of John stored in database: Developer and Web Developer
title list submitted from the users: Web Developer and New Title
We compare the two title lists and figure out: we shall delete Developer title and add New Title on John.
You could add hidden fields before the select menu which will cause 0/falsey values to also be sent to PHP.
<input type="hidden" name="stuff[]" value="0" />
<input type="hidden" name="stuff[]" value="0" />
<input type="hidden" name="stuff[]" value="0" />
<select multiple name="stuff[]">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
This is similar to how you would send unchecked check boxes to the server:
https://stackoverflow.com/a/1992745/268074 (note the answer without JS)
You can dump your old values in a hidden select, so you'll get them when handling your form.
<!-- Invisible select field -->
<select multiple name="oldData[]" style="display:none;" aria-hidden="true">
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3" selected>3</option>
</select>
<!-- Real select field -->
<select multiple name="data[]">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
Note: I was inspired by #Petah's answer, don't forget to upvote him :-).
Let's say we have $old_titles list with titles from db, and $new_titles titles from submit.
$add = array_diff($new_titles, $old_titles);
$delete = array_diff($old_titles, $new_titles);
$add - list of titles to add, $delete - list of titles to delete.
Add action:
$dbh->prepare('INSERT INTO table3(idName, idTitle) VALUES (:idName, :idTitle)');
foreach($add as $titleId) {
$dbh->execute(['idName' => $userId, 'idTitle' => $titleId]);
}
Delete action:
$dbh->prepare('DELETE FROM table3 WHERE idName = :idName AND idTitle = :idTitle');
foreach($delete as $titleId) {
$dbh->execute(['idName' => $userId, 'idTitle' => $titleId]);
}
Not sure why everybody keeps using hidden input fields to track the existing data. When you POST your form, you can just retrieve the original values from the database. That's a lot safer then depending on user input, because even hidden fields can be altered. I know it won't matter too much in this particular situation, but I think it's best to always use the best practise.
Now in PHP you have two options. The first would be to delete all relations and then add the new relations. The second would be to delete all that wasn't posted and add/update the relations that where.
Although the second options might seem to be the "least destructive" as you call it, it is the easiest and least error prone way. The only reason I would abandon this tactic is when I would store extra data in the relations table. EG: date added (when did somebody get into a function) or added by user.
How would you go about it (method 2)?
Keep in mind that I use a method that tries to continue whenever possible. Another approach would be to halt when false data is posted.
<?php
if (array_key_exists("relations",$_POST) && is_array($_POST["relations"])) {
//no duplicates, you dont want to store the same function twice
//only integers
$list = array_unique(array_filter($_POST["relations"], "is_int"));
$csv = implode(',', $list);
//get the userid hoewever you normally would
$userId = 1
//Delete existing
$query = "DELETE FROM Table3 WHERE idname=".$userId
//insert all posted functions
//I select the id's from the actual table so it will never inserted a none existing ID.
//this approach never has duplicates, so the duplicate check in the beginning is redundant now, but I leave it incase I change this.
$query = "INSERT INTO Table3 (idName, idTitle) SELECT ".$userId.", Id FROM Table2 WHERE id IN (".$csv.")".
//execute queries
} else {
//nothing posted, delete all relations
}
?>
Now if you only want to insert new data and remove none-existing
<?php
if (array_key_exists("relations",$_POST) && is_array($_POST["relations"])) {
//no duplicates, you dont want to store the same function twice
//only integers
$list = array_unique(array_filter($_POST["relations"], "is_int"));
$csv = implode(',', $list);
//get the userid hoewever you normally would
$userId = 1
//Delete existing
$query = "DELETE FROM Table3 WHERE idname=".$userId." AND idTitle NOT IN (".$csv.")"
//only insert new functions
//I select the id's from the actual table so it will never inserted a none existing ID.
//this approach never has duplicates, so the duplicate check in the beginning is redundant now, but I leave it incase I change this.
$query = "INSERT INTO Table3 (idName, idTitle) SELECT ".$userId.", Id FROM Table2 WHERE id IN (".$csv.") AND id NOT IN (SELECT idTitle FROM table3 WHERE idName=".$userId.")".
//execute queries
} else {
//nothing posted, delete all relations
}
?>
The only better way is the simplest one, remember KIS
on update you just need to do following
DELETE FROM table3 WHERE idName=??
and then insert all selected idTitles again in table3
This can be done in two SQL statement without knowing which value has been de-selected. Following these 2 steps
1) In your PHP, create a comma separated list of all selected option. For example: 2, 3.
2) Then execute the following statements:
DELETE FROM tblLink WHERE idName = $id AND idTitle NOT IN ($list);
INSERT IGNORE tblLink (idName, idTitle)
SELECT $id, id FROM tblTitle WHERE id IN ($list);
Note: for shake of simplification, I use $id and $list in my example. When you intent to use it, you should properly prepare and bind parameter properly. Make sure that idName and idTitle are primary key to make it work.
To my opinion you have to check what actions should be performed first. This is a small example that you can run here http://sandbox.onlinephpfunctions.com/code/0fa11b3c9f846e344c912f5a3e994eea5e9cac34.
First run a select on Table 3 for idName=1 the output would be an array of idTitle's (1,2). In the following example the $array1 is the output of such a select statement and $array2 is the array of new idTitle's for the same idName that will have to be updated or deleted or inserted. If for example the new idTitles for a user are more or less than the previous ones then there you should have some sort of control over it in order to decide what queries you will run to the database.
$array1 = array(1,7,3,5);// initial idTitles try it on the above link (php sandbox) with more or less values
$array2 = array(1,4,3,6);//new idTitles try it on the above link (php sandbox) with more or less values
$count1=count($array1);
$count2=count($array2);
$diff=$count1-$count2;
if ($diff==0) {
$result1=array_values(array_diff_assoc($array2,$array1));
$result2=array_values(array_diff_assoc($array1,$array2));
$countr=count($result1);// or $result2 it is the same since they have the same length
$cr=0;
while($cr < $countr) {
print_r("update tblname set val=$result1[$cr] where id=theid and val=$result2[$cr]\n");
$cr++;
}
}
if ($diff!=0) {
$result1=array_diff($array2,$array1);
$result2=array_diff($array1,$array2);
if(count($result2)>0) {
foreach($result2 as $r2) {
print_r("delete from tblname where id=theid and val=$r2\n");
}
}
foreach($result1 as $r1) {
print_r("insert into tblname where id=theid and val=$r1\n");
}
}
How I would do it would extend the 1st table
id | Name | Title IDs
1 | John | 1,2
2 | Smith | 1,3
Each time the row is updated I would simply just replace the title_ids fields and be done with it.