Meteor Join Collection

I’ve 2 Collection which Address & Users each user Can have multiple address I want to show the name of user & his address in a table… Users collection:

{
_id:
clientName:
}

Address collection:

{
addressId:
addressName:
clientId
}

I don’t use my query in publish, my query is in report generation.

Since your Address collection is referencing the clientId this implies that each address is only for one client, i.e. the same address cannot be shared by multiple clients.

In that case you should get rid of the Address collection and simply embed the addresses within the user document. This is frowned upon in SQL, but is actually the recommended way with mongo: Model One-to-Many Relationships with Embedded Documents.

{
  _id: ...
  clientName: ...
  addresses: [
    {address: "123 Main Street", city: "Springfield",   ...  },
    {address: "PO Box 1234",  ... },
    ...
  ],
  ...
}
1 Like

While the response above is correct (every address is associated directly with a user, so it might as well be in an array of embedded documents), if you don’t want to change your model you could use the $lookup operator with the model you specified.

https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/

You might need to use Users.rawCollection() or use the mongodb official driver, in order to utilize Mongo’s aggregation functions. This might look something like:

async function usersWithAddresses() {
   const collection = Users.rawCollection()
   const result = await collection.aggregate([
      {
          $lookup: {
            from: 'address',
            localField: '_id',
            foreignField: 'clientId',
            // This will output an array of results on the field 'addresses'
            as: 'addresses'
         }
      }
   ]).toArray()
   return result
}

So this query is creating a field addresses in the query results that is an array of embedded documents. The data for this field comes from another collection, so you might say it’s a kind of join…

1 Like

Is there any way to use this in Publications/Subscriptions? @jonlachlan

Meteor.publish("fetch_all_user_trips",async  function (userId){
	// var allTrips = Trips.find({}).fetch();		
	var allTrips =  Trips.rawCollection();
	const result = await allTrips.aggregate([
	{
	  $lookup: {
	    from: 'user_data',
	    localField: 'created_by',
	    foreignField: 'user_id',
	    as: 'users_details'
	 }
	}
	]).toArray();
    var self = this;
    console.log(result);
   for(var i=0;i<result.length;i++){
      self.added('user_trips',result[i]._id, result[i]);
   }
    self.ready(); 
})

Tried this, and it does not works!

Binding between User and Trips


Meteor.publish("fetch_all_user_trips",  function (userId){
	// var allTrips = Trips.find({}).fetch();		
	var self = this;
	var allTrips =  Trips.rawCollection();
	const result = allTrips.aggregate([
	{
	  $lookup: {
	    from: 'user_data',
	    localField: 'created_by',
	    foreignField: 'user_id',
	    as: 'users_details',
	 }
	},
	
	{ $project : { 
					trip_id : 1 , 
					trip_name : 1 , 
					start_date : 1,
					end_date : 1,
					location : 1,
					city : 1,
					state : 1,
					lat : 1,
					long : 1,
					country : 1,
					users_details : 1
					 } }
	]).toArray() .then(function(result) {
	    console.log(result); // "initResolve"
	   for(var i=0;i<result.length;i++){
	      self.added('user_trips',result[i]._id, result[i]);
	   }
	    self.ready();
  })
     
})

Got this working using the above code. Is there any method to use project in the users collections as well, as there are many unused data.

Thanks @jonlachlan

@mixmatric looking at join conditions and uncorrelated sub-queries section in the Mongo docs for $lookup, it might look something like:

const result = allTrips.aggregate([
   {
     $lookup: {
       from: 'user_data',
       // Per docs, "The pipeline cannot directly access the input document fields. 
       // Instead, first define the variables for the input document fields, 
       // and then reference the variables in the stages in the pipeline."
       let: { created_by: "$created_by" },
       pipeline: [
         $match: {
           $expr: { $eq: [ "$user_id", "$created_by" ] }
         },
         $project: { 
           // Fields you want to include/exclude
           emails: 1 
         }
       ]
       as: 'users_details',
    }
   },
   // ...
}].toArray()

@mixmatric Actually you should be able to specify the field(s) on user_details in your original $project object by simply specifying the field path like 'user_details.emails': 1. That way you can use the simpler join using localField and foreignField.

Thanks @jonlachlan, will do the same

I have been using this package with great success so far (my collections are not huge, just few thousands of records)
https://atmospherejs.com/reywood/publish-composite

In one of my queries there are three levels of nesting, so I noticed that loading big dataset can take a while… once it starts really bothering me, I will try to experiment with $lookup, whether that would be faster.

In my other projects, I started off with nesting the documents as arrays and now I am at the stage when I hate it, because querying such dataset for reporting is very slow. I basically have to unwind all the documents beforehand.

You can take a look at grapher. Its very easy to use but can require some code refactoring.

But you van benefit of using publish only where you need reactive data and methods where you dont.