Laravel Group and Show Popular Relation - mysql

If I have a users table with a column country and the user belongsToMany Categories, Is there a way I can group the users by country showing the most popular categories for each country?
For example:
Users
id | name | country
1 | UserA | Canada
2 | UserB | USA
3 | UserC | Canada
Categories
id | Name
1 | Housing
2 | Cooking
category_user
id | category_id | user_id
1 | 1 | 1
2 | 2 | 2
3 | 1 | 3
From the table you can tell that Housing is the most popular category in Canada. I can't seem to show that with code.
I've tried eloquent "with" function but it only just shows me category for a certain (1st?) user.
Any help would be greatly appreciated!
Edit:
Ersoy gave me a fantastic solution but it brought up a new problem which I didn't consider when posting the question. I have another table "payments" with a haveMany relationship where I sum all the amount column for each user. With the current join query, it duplicates the amount for each belongsToMany relationship resulting in wrong sum.
Payments
id | amount | user_id
1 | 500 | 1
2 | 200 | 2
3 | 150 | 1
4 | 100 | 3

return User::leftJoin('category_user as cu', 'users.id', '=', 'cu.user_id')
->join('categories as c', 'cu.category_id', '=', 'c.id')
->groupBy(['country', 'c.name'])
->get([
'users.country',
'c.name',
DB::raw('count(*) as total')
])
->groupBy('country')
->values()
->map(function (Collection $elt) {
return $elt->sortByDesc('total')->first();
});
it will print in the following format.
[
{
"country": "Canada",
"name": "Housing",
"total": 2
},
{
"country": "USA",
"name": "Cooking",
"total": 1
}
]

Related

Laravel Eloquent multiple whereHas on relationship where column 'order' is higher than the previous whereHas

I'm working on a bus route system with stops at certain locations.
The route stops are in a ascending sequence by a 'order' column, as in 1, 2, 3, 4, 5.. etc.
These are my tables:
BUS
id | operator_id | name
1 | 1 | The Big Red Bus
PLACES (some dummy data just for example)
id | name | slug | parent_id
1 | Amsterdam | amsterdam |
2 | London | london |
3 | Stockholm | stockholm |
4 | Helsinki | helsinki |
5 | Dam Square | dam-square | 1
ROUTES
id | name
1 | Amsterdam - London
2 | London - Amsterdam
ROUTE_LOCATIONS (LOCATIONS)
id | route_id | place_id | order | start | end
1 | 1 | 1 | 1 | 1 | 0
2 | 1 | 2 | 2 | 0 | 0
3 | 1 | 3 | 3 | 0 | 0
4 | 1 | 4 | 4 | 0 | 1
User Input
The given user input when I start a search to check if we have any available bus routes is a place slug from the PLACES table.
For example:
from: london
to: dam-square
Below is the query what I tried so far, but I am really unsure on how to build a check/join into the actual query to check if the 'order' sequence is ascending. I just can't get my head around it.
$buses = Bus::whereHas('route.locations.place', function ($query) use ($from, $to) {
$query->where('slug', $from)->where('end', 0);
})->whereHas('route.locations.place', function ($query) use ($from, $to) {
$query->where('slug', $to)->where('start', 0);
})->get();
Relationships structure is as follows:
BUS hasOne ROUTE
ROUTE belongsToMany BUS
ROUTE hasMany LOCATIONS (ROUTE_LOCATIONS TABLE)
I have already tried the following query which works to get available routes, but I really like to do it directly in laravel eloquent with my models, so I can easily use the relationships in my view. I am just unsure on how to go about it.
Below query only works with a place ID instead of a slug, and I really like it to be a slug instead of an ID.
$routes = DB::select('SELECT R.id, R.name
FROM route_locations L
INNER JOIN routes R ON R.id = L.route_id
WHERE
L.place_id = "'.$from->id.'" AND
EXISTS (SELECT id FROM route_locations F WHERE L.route_id = F.route_id AND F.order > L.order AND F.place_id = "'.$to->id.'")');
Does anyone know if this is possible and how?
Thank you!
Your code could be rewritten to this one:
$busses = Bus::whereHas('route.locations.place', function ($query) use ($from, $to) {
$query->where('slug', $from)
->where('slug', $to)
->where('start', 0)
->where('end', 0);
// here should do a check if the 'order' column is higher than the first wherehas
})->get();
And it doesn't make much sense to me.

Need help building an odd Sequelize query

Some context:
I have two tables: challenges and participants.
challenges table columns: id, userId, and name.
participants table columns: id, userId, and challengeId
The userId column on the challenge table indicates ownership of a challenge.
The participants table is simply an association table to keep track of users who have joined challenges.
challenges.hasMany(participants)
participants.belongsTo(challenges)
The owner of a challenge may or may not be a participant.
My question:
How can I construct a single Sequelize query to find all owned OR joined challenges for a specific user?
Example Data:
challenges
+----+--------+-------------+
| id | userId | name |
+----+--------+-------------+
| 1 | 2001 | Challenge 1 |
| 2 | 2002 | Challenge 2 |
+----+--------+-------------+
participants
+----+--------+-------------+
| id | userId | challengeId |
+----+--------+-------------+
| 1 | 2001 | 1 |
| 2 | 2002 | 1 |
+----+--------+-------------+
Desired result of finding all owned OR joined challenges of user 2002:
challenges: [
{
id: 1,
userId: 2001,
name: "Challenge 1"
},
{
id: 2,
userId: 2002,
name: "Challenge 2"
}
]
Give this a try:
SELECT challenges.*
FROM challenges
LEFT JOIN participants
ON challenges.id = participants.challengeId
WHERE challenges.userId = 2002
OR participants.userId = 2002;

use where clauses to get data from join query in Laravel

I am creating a system using Laravel and AngularJS where I assign tasks to users. Multiple tasks has multiple users and vice versa. In the Database, I have this tables:
task:
id | name
task_users:
id | task_id | user_id
users:
id | name
In my view, I display a particular task, using id of task table. I display a list of users (called unassigned users) who are not assigned to that particular task. When that user is assigned, it's name gets removed from the list.
To achieve this, I used this query:
public static function remainingUser($task_id)
{
return \DB::table('users')
->leftjoin('task_users', 'task_users.user_id', '=', 'users.id')
->select('users.id',
'users.name as name'
)
->where('task_id', '!=', $task_id)
->orWhere('task_id', null)
->get();
}
Suppose I have this data
task:
id | name
1 | Task1
2 | Task2
3 | Task3
users:
id | name
1 | User1
2 | User2
3 | User3
4 | User4
5 | User5
6 | User6
7 | User7
8 | User8
9 | User9
10 | User10
task_users:
id | task_id | user_id
1 | 1 | 1
1 | 1 | 2
1 | 1 | 3
1 | 1 | 5
1 | 1 | 6
1 | 1 | 7
1 | 2 | 2
1 | 2 | 4
Now suppose I am displaying task details of task_id = 1 and I want to assign user 4 to this task. So my list of unassigned users should contain all the users who are not assigned this task. My query does not return the required data. I have also tried different conditions in where clause, but I do not get the correct required data. What am I doing wrong?
The issue exists because when you select from Users, you get all users left Joined with a single instance of tasks. So if User1 has Task1 and Task2, only Task1 will be matched here, because it will match User1 to the 1st row found for him within task_users. In order to list the unasigned users, your query would look similar to this:
public static function remainingUser($task_id)
{
return \DB::table('task_users')
->rightJoin('users', 'task_users.user_id', '=', 'users.id')
->select(
\DB::raw("DISTINCT(`users`.`id`)"),
'users.name as name'
)
->where('task_id', '!=', $task_id)
->orWhere('task_id', null)
->get();
}

Link to multiple foreign rows from one row

I really don't know how this is called so I couldn't find an answer.
For example I have the following tables:
Table products Table users
+----------+----------+----------+ +----------+----------+
| id | name | price | | username | products |
+----------+----------+----------+ +----------+----------+
| 1 | Bread | 1.5 | | James | 1, 2 |
+----------+----------+----------+ +----------+----------+
| 2 | Cookies | 2.0 | | David | 2, 3 |
+----------+----------+----------+ +----------+----------+
| 3 | Milk | 1.2 | | Paul | 3, 1 |
+----------+----------+----------+ +----------+----------+
products.id is the foreign key for users.products.
I would like to get the name and price of all David's products using a MySQL query. This should return the following:
+----------+----------+
| name | price |
+----------+----------+
| Cookies | 2.0 |
+----------+----------+
| Milk | 1.2 |
+----------+----------+
The JOIN function would be the best to use I guess, but David can have just 1 or 100 products. Creating 100 columns to fit all products doesn't sound very efficient.
How can I realize this?
Currently I'm just fetching everything and filter it using PHP, this is not a problem at the moment but as the tables will grow I guess this will be very inefficient.
This could be solved by changing your data model.
users
+----------+----------+
| id | username |
+----------+----------+
| 1 | Fred |
+----------+----------+
| 2 | John |
+----------+----------+
products
+----------+----------+----------+
| id | name | price |
+----------+----------+----------+
| 1 | Bread | 1.5 |
+----------+----------+----------+
| 2 | Cookies | 2.0 |
+----------+----------+----------+
| 3 | Milk | 1.2 |
+----------+----------+----------+
And here comes the magic: You could connect the two tables using a third table:
user_procuct_connections
+----------+----------+------------+
| id | user_id | product_id |
+----------+----------+------------+
| 1 | 1 | 2 | -> Fred has Cookies
+----------+----------+------------+
| 2 | 1 | 3 | -> Fred also has Milk
+----------+----------+------------+
| 3 | 2 | 1 | -> John has Bread
+----------+----------+------------+
If you want a user to be able to own a single product only, then you can remove the id column, an make the user_id and product_id primary key together.
Then when you want to get for example all of Freds products then just
SELECT
*
FROM
products
WHERE
id IN (
SELECT
product_id
FROM
user_procuct_connections
WHERE
user_id = 1
)
You could try this:
SELECT * FROM products pt
where FIND_IN_SET(pt.id,(select us.prices from users us
WHERE us.username = "David"));
Working fiddle: http://sqlfiddle.com/#!2/4f78d/2
The design for the 'Users' table is wrong. Instead of working around this bad design, please change the design.
So effectively, you design could be as such:
Table Products :
ID Name Price
1 Bread 1.5
2 Cookies 2.0
3 Milk 1.2
Table users :
Username Products
James 1
James 2
David 2
David 3
Paul 3
Paul 1
You see, you can have multiple rows for each user i.e.one row for each product in the Users table. You can maintain another Boolean field to identify which ones are currently 'active' or 'applicable'. Joins' would be much easier in that case. Also, updating records would also be easier. In case, in future you'd want to retrieve & analyse data from historic records, that would be possible too!!!
Another thing with the current design is that times and again you'd have to work around the infamous "comma-seperated values". Let's say you have a record as such:
Username Products
James 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
If you need to sort this data into rows, you'd have to work-around the 'Comma-seperated' values so many times. Imagine doing that for 'n' number of records!!
Hope this helps!!!
If you join the tables like:
SELECT name, price FROM products AS p INNER JOIN user_products AS up ON up.id = p.product_id AND up.user_id = <davids id>
You will get an array where first column will be the name and the second the price. You can then do what you want with it in PHP and change the array. You don't have to filter anymore.

can couchdb do loops

Can couchdb do loops?
Let's say I have a database of interests that have 3 fields
subject1,subject2,subject3. example, cats,nutrition,hair or space,telescopes,optics etc.
A person (A) has 10 interests composed of 3 fields each.
10 more people B,C,D...have 10 interests each composed of 3 subjects each.
When person A logs in I want the system to search for all people with matching interests.
In javascript I would normally loop through all the interests and then find matching ones I guess using
two loops. Then store the matches in another database for the user like "matchinginterests".
Is there any easy way to do this in couchdb compared to mysql -- which seems very complicated.
Thanks,
Dan
I think I understand what you are asking. The answer is pretty straightforward with Map/Reduce.
Say you have the following people documents:
{
"name": "Person A",
"interests" [ "computers", "fishing", "sports" ]
}
{
"name": "Person B",
"interests" [ "computers", "gaming" ]
}
{
"name": "Person C",
"interests" [ "hiking", "sports" ]
}
{
"name": "Person D",
"interests" [ "gaming" ]
}
You would probably want to emit your key as the interest, with the value as the person's name (or _id).
function (doc) {
for (var x = 0, len = doc.interests.length; x < len; x++) {
emit(doc.interests[x], doc..name);
}
}
Your view results would look like this:
computers => Person A
computers => Person B
fishing => Person A
gaming => Person B
gaming => Person D
hiking => Person C
sports => Person A
sports => Person C
To get a list of people with computers as an interest, you can simply send key="computers" as part of the query string.
If you want to add a reduce function to your map, you can simply use _count (shortcut to use a compiled reduce function) and you can retrieve a count of all the people with a particular interest, you can even use that to limit which interests you query to build your relationships.
When person A logs in I want the system to search for all people with matching interests.
SELECT i_them.* FROM interests AS i_me
INNER JOIN interests AS i_them ON (i_them.person != i_me.person) AND
((i_them.subject1 IN (i_me.subject1, i_me.subject2, i_me.subject3)) OR
(i_them.subject2 IN (i_me.subject1, i_me.subject2, i_me.subject3)) OR
(i_them.subject3 IN (i_me.subject1, i_me.subject2, i_me.subject3)))
WHERE i_me.person = 'A'
Is that what you wanted to do?
If you design your tables a little smarter though you'd do it like
SELECT DISTINCT them.* FROM person AS me
INNER JOIN interest AS i_me ON (i_me.person_id = me.id)
INNER JOIN interest AS i_them ON (i_them.subject = i_me.subject)
INNER JOIN person AS them ON (them.id = i_them.person.id AND them.id != me.id)
WHERE me.name = 'A'
Using the following tables
table interest
id integer primary key autoincrement
person_id integer //links to person table
subject varchar //one subject per row.
+-----+-----------+---------+
| id | person_id | subject |
+-----+-----------+---------+
| 1 | 3 | cat |
| 2 | 3 | stars |
| 3 | 3 | eminem |
| 4 | 1 | cat |
| 5 | 1 | dog |
| 6 | 2 | dog |
| 7 | 2 | cat |
table person
id integer primary key autoincrement
name varchar
address varchar
+-----+------+---------+
| id | name | address |
+-----+------+---------+
| 1 | A | here |
| 2 | Bill | there |
| 3 | Bob | everyw |
result
+-----+------+---------+
| id | name | address |
+-----+------+---------+
| 2 | Bill | there |
| 3 | Bob | everyw |
This is how (what you call) 'looping' in SQL works...
First you take person with name 'A' from the table.
me.id me.name me.address
| 1 | A | here |
You look up all the interests
me.id me.name me.address i_me.subject
| 1 | A | here | cat
| 1 | A | here | dog
Then you match everyone elses interests
me.id me.name me.address i_me.subject i_them.subject i_them.person_id
| 1 | A | here | cat | cat | 3
| 1 | A | here | cat | cat | 2
| 1 | A | here | dog | dog | 2
And then you match the person to them's interest (except for me of course)
me.id me.name me.address i_me.subject i_them.subject i_them.person_id them.name
| 1 | A | here | cat | cat | 3 | Bob
| 1 | A | here | cat | cat | 2 | Bill
| 1 | A | here | dog | dog | 2 | Bill
Then you return only the data from them and 'throw' the rest away, and remove duplicate rows DISTINCT.
Hope this helps.