Special where on embedded relations - Laravel MongoDB - database

I have the following users collection:
{
"type": "provider",
"name": "user name",
"username": "username",
"password": "$2y$10$D3z0tLwOwB0tqPEnl63VuexOwqcR75QkVILemB1.TEsAJlk6Ixwim",
"specialties": [
"specialty 1",
"specialty 2"
]
}
and a specialties collection :
{
"_id": "5b26103b2df243228c0003ea",
"title": "specialty 1",
"description": "specialti 1 desc",
},{
"_id": "5b26103b2df243228c0003ea",
"title": "specialty 2",
"description": "specialti 2 desc",
},
The relation between them is embedded-many and here is my relationship in User model,
public function specialties()
{
return $this->embedsMany(Specialty::class, 'specialties', 'title');
}
I want to filter the users by specialty. For example, the above JSON user object should be returned if the filtered specialty is "specialty 1".
I know about non-embedded collections but my data is saved on my database and I cannot not change the schema.
Are there any alternative solutions?

The solution was in the documentation, MongoDB specific operators,
In my case, the answer is,
$providers = User::where('specialties', 'all', ['specialty 1'])->with('s_specialties')->get();
This code simulates $in operator in MongoDB. More about operators.

Related

Convert SQLite to MongoDB

I have a normalized SQLite database and I want to convert to one collection MongoDB database.
let's take an example:
We suppose that my SQLite database looks like :
SQLite database tables
I want my MongoDB database looks like :
{
"_id" : ObjectId("5efdf2c2b268674c2bf74e85"),
"firstName": "robert",
"lastName": "kas",
"addresses":[
{
"address": "This is address 1",
"type": "home",
},
{
"address": "This is address 2",
"type": "work",
}
]
}

Update mongo array elements field from its other field

Let's assume following document
{
"companies": [
{"id": 1, "name": "Company 1", "new_name": "New company 1"},
{"id": 2, "name": "Company 2", "new_name": "New company 2"}
]
}
Is there a way I can reference element of array by itself to migrate to scheme
{
"companies": [
{"id": 1, "name": "New company 1", "new_name": "New company 1"},
{"id": 2, "name": "New company 2", "new_name": "New company 2"}
]
}
within a single call to update without scripting with forEach in mongo 4.2?
Something like
update(
...
{
"companies.$[].name": "$companies.<???>.new_name"
}
)
To access another field's value in a query you need to use aggregation, taking advantage of executing aggregation pipeline in .update()'s which got introduced in MongoDB version 4.2, try below query :
update(
...
[
{
$addFields: { // will replace existing field with new value
companies: {
$map: { // Iterates on an array & creates a new one
input: "$companies",
in: { $mergeObjects: [ "$$this", { name: "$$this.new_name" } ] } // `mergeObjects` will merge two objects `name` field will be replace in current object i.e; `$$this`
}
}
}
}
]
)
Test : You can test aggregation pipeline here : mongoplayground
Note : Seems to be using aggregation is only option to do this in one call, but downside could be only if your array is too huge, cause $map has to re-write entire array again.

cloudant groupby and count number of times a value appears

First time working with a nosql DB and having trouble writing a query that can look in my DB and for a key count the number of time it appears by another key.
For instance if my DB contains
{
"person": "user1",
"status": "good"
},
{
"person": "user1",
"status": "good"
},
{
"person": "user1",
"status": "bad"
},
{
"person": "user2",
"status": "good"
}
would like to know that person1 was good 2 and bad 1 and person2 was only good 1
in sql would do
select person, status, count(*)
from mydb
groupby person, status
or to get it by a user in the db
select person, status, count(*)
from mydb
groupby person, status
where person = "user1"
You can achieve this with Cloudant's MapReduce views and suitably chosen query parameters. I created a view where the map is
function (doc) {
emit([doc.person, doc.status], null);
}
and the reduce the built-in _count. That gives us an index where the key is a vector, and we can then group at different levels. Using groupby=true with group_level=2 gives us the desired result:
curl 'https://A.cloudant.com/D/_design/so/_view/by-status?groupby=true&group_level=2'
{
"rows": [
{
"key": [
"user1",
"bad"
],
"value": 1
},
{
"key": [
"user1",
"good"
],
"value": 2
},
{
"key": [
"user2",
"good"
],
"value": 1
}
]
}

How to do a NoSql linked query

I have a noSql (Cloudant) database
-Within the database we have documents where one of the document fields represents “table” (type of document)
-Within the documents we have fields that represent links other documents within the database
For example:
{_id: 111, table:main, user_id:222, field1:value1, other1_id: 333}
{_id: 222, table:user, first:john, other2_id: 444}
{_id: 333, table:other1, field2:value2}
{_id: 444, table:other2, field3:value3}
We want of way of searching for _id:111
And the result be one document with data from linked tables:
{_id:111, user_id:222, field1:value1, other1_id: 333, first:john, other2_id: 444, field2:value2, field3:value3}
Is there a way to do this?
There is flexibility on the structure of how we store or get the data back—any suggestions on how to better structure the data to make this possible?
The first thing to say is that there are no joins in Cloudant. If you're schema relies on lots of joining then you're working against the grain of Cloudant which may mean extra complication for you or performance hits.
There is a way to de-reference other documents' ids in a MapReduce view. Here's how it works:
create a MapReduce view to emit the main document's body and its linked document's ids in the form { _id: 'linkedid'}
query the view with include_docs=true to pull back the document AND the de-referenced ids in one go
In your case, a map function like this:
function(doc) {
if (doc.table === 'main') {
emit(doc._id, doc);
if (doc.user_id) {
emit(doc._id + ':user', { _id: doc.user_id });
}
}
}
would allow you to pull back the main document and its linked user document in one API by hitting the GET /mydatabase/_design/mydesigndoc/_view/myview?startkey="111"&endkey="111z"&include_docs=true endpoint:
{
"total_rows": 2,
"offset": 0,
"rows": [
{
"id": "111",
"key": "111",
"value": {
"_id": "111",
"_rev": "1-5791203eaa68b4bd1ce930565c7b008e",
"table": "main",
"user_id": "222",
"field1": "value1",
"other1_id": "333"
},
"doc": {
"_id": "111",
"_rev": "1-5791203eaa68b4bd1ce930565c7b008e",
"table": "main",
"user_id": "222",
"field1": "value1",
"other1_id": "333"
}
},
{
"id": "111",
"key": "111:user",
"value": {
"_id": "222"
},
"doc": {
"_id": "222",
"_rev": "1-6a277581235ca01b11dfc0367e1fc8ca",
"table": "user",
"first": "john",
"other2_id": "444"
}
}
]
}
Notice how we get two rows back, the first is the main document body, the second the linked user.

MongoDB Array Query Performance

I'm trying to figure out what the best schema is for a dating site like app. User's have a listing (possibly many) and they can view other user listings to 'like' and 'dislike' them.
Currently i'm just storing the other persons listing id in a likedBy and dislikedBy array. When a user 'likes' a listing, it puts their listing id into the 'liked' listings arrays. However I would now like to track the timestamp that a user likes a listing. This would be used for a user's 'history list' or for data analysis.
I would need to do two separate queries:
find all active listings that this user has not liked or disliked before
and for a user's history of 'liked'/'disliked' choices
find all the listings user X has liked in chronological order
My current schema is:
listings
_id: 'sdf3f'
likedBy: ['12ac', 'as3vd', 'sadf3']
dislikedBy: ['asdf', 'sdsdf', 'asdfas']
active: bool
Could I do something like this?
listings
_id: 'sdf3f'
likedBy: [{'12ac', date: Date}, {'ds3d', date: Date}]
dislikedBy: [{'s12ac', date: Date}, {'6fs3d', date: Date}]
active: bool
I was also thinking of making a new collection for choices.
choices
Id
userId // id of current user making the choice
userlistId // listing of the user making the choice
listingChoseId // the listing they chose yes/no
type
date
I'm not sure of the performance implications of having these choices in another collection when doing the find all active listings that this user has not liked or disliked before.
Any insight would be greatly appreciated!
Well you obviously thought it was a good idea to have these embedded in the "listings" documents so your additional usage patterns to the cases presented here worked properly. With that in mind there is no reason to throw that away.
To clarify though, the structure you seem to want is something like this:
{
"_id": "sdf3f",
"likedBy": [
{ "userId": "12ac", "date": ISODate("2014-04-09T07:30:47.091Z") },
{ "userId": "as3vd", "date": ISODate("2014-04-09T07:30:47.091Z") },
{ "userId": "sadf3", "date": ISODate("2014-04-09T07:30:47.091Z") }
],
"dislikedBy": [
{ "userId": "asdf", "date": ISODate("2014-04-09T07:30:47.091Z") },
{ "userId": "sdsdf", "date": ISODate("2014-04-09T07:30:47.091Z") },
{ "userId": "asdfas", "date": ISODate("2014-04-09T07:30:47.091Z") }
],
"active": true
}
Which is all well and fine except that there is one catch. Because you have this content in two array fields you would not be able to create an index over both of those fields. That is a restriction where only one array type of field (or multikey) can be be included within a compound index.
So to solve the obvious problem with your first query not being able to use an index, you would structure like this instead:
{
"_id": "sdf3f",
"votes": [
{
"userId": "12ac",
"type": "like",
"date": ISODate("2014-04-09T07:30:47.091Z")
},
{
"userId": "as3vd",
"type": "like",
"date": ISODate("2014-04-09T07:30:47.091Z")
},
{
"userId": "sadf3",
"type": "like",
"date": ISODate("2014-04-09T07:30:47.091Z")
},
{
"userId": "asdf",
"type": "dislike",
"date": ISODate("2014-04-09T07:30:47.091Z")
},
{
"userId": "sdsdf",
"type": "dislike",
"date": ISODate("2014-04-09T07:30:47.091Z")
},
{
"userId": "asdfas",
"type": "dislike",
"date": ISODate("2014-04-09T07:30:47.091Z")
}
],
"active": true
}
This allows an index that covers this form:
db.post.ensureIndex({
"active": 1,
"votes.userId": 1,
"votes.date": 1,
"votes.type": 1
})
Actually you will probably want a few indexes to suit your usage patterns, but the point is now can have indexes you can use.
Covering the first case you have this form of query:
db.post.find({ "active": true, "votes.userId": { "$ne": "12ac" } })
That makes sense considering that you clearly are not going to have both an like and dislike option for each user. By the order of that index, at least active can be used to filter because your negating condition needs to scan everything else. No way around that with any structure.
For the other case you probably want the userId to be in an index before the date and as the first element. Then your query is quite simple:
db.post.find({ "votes.userId": "12ac" })
.sort({ "votes.userId": 1, "votes.date": 1 })
But you may be wondering that you suddenly lost something in that getting the count of "likes" and "dislikes" was as easy as testing the size of the array before, but now it's a little different. Not a problem that cannot be solved using aggregate:
db.post.aggregate([
{ "$unwind": "$votes" },
{ "$group": {
"_id": {
"_id": "$_id",
"active": "$active"
},
"likes": { "$sum": { "$cond": [
{ "$eq": [ "$votes.type", "like" ] },
1,
0
]}},
"dislikes": { "$sum": { "$cond": [
{ "$eq": [ "$votes.type", "dislike" ] },
1,
0
]}}
])
So whatever your actual usage form you can store any important parts of the document to keep in the grouping _id and then evaluate the count of "likes" and "dislikes" in an easy manner.
You may also not that changing an entry from like to dislike can also be done in a single atomic update.
There is much more you can do, but I would prefer this structure for the reasons as given.

Resources