Creating a many to many relationship in a complex

2019-09-18 20:47发布

问题:

I have 3 collections, meteor.user(), Categories and Posts.

A user can choose to follow any category's posts by saving the id of the category in an array in meteor.user()

The end goal of this is that the user has a Personal stream where only the posts in the categories they are are following show up.

Each post inside the Posts collection has within it an array of the Categories it's featured in.

and each category has within it an array of posts, each post in the array has a field of ID of the post.

Where id: 67 is the id of the category, and posts.ID: 74 is the id of a post within the category.

below is the id of the post in Posts collection with matches posts.ID in the category collection

The User saves a category id so it appears as the category array in meteor.user()

When I do this

Template.userTimeline.helpers({
  category: function() {
    var posts = Category.find({
      id: {
        $in: Meteor.user().category
      }
    });
    return posts
    console.log(posts);
  }
});

I can do this

 {{#each category}}
  <ul class="article-timeline">
    {{#each posts}} {{ID}} {{/each}}
  </ul>
{{/each}}

But this lets me access the posts.ID within the category collection and display it.

PROBLEM

How can I go further and match posts.ID I got with the above code, to the the id of a post in Post so that I can access the posts from the Posts collection. Could i do something like this?

Template.userTimeline.helpers({
  category: function() {
    var postsInCategory = Category.find({
      id: {
        $in: Meteor.user().category
      }
    });

    var postMatch = Post.find({
      ID: {
        $in: postsInCategory
      }
    });
    return postMatch
    console.log(postMatch);
  }
});

FURTHER EDIT AFTER ZIM ANSWER

So the solution you have given me is taking post.title from the post object contained within my Categories collection as shown below.

This post array within the Category collection is incomplete, its a copy of an original Posts Collection

what I need to do is use post.ID as shown in the image above. i.e. the post array within the Categories Collection, to create a many to many relationship with the id of a post within the original Posts Collection so, instead of having post.title I need to have title.rendered as below.

Posts Collection.

If you see that the ID (not the top id, which is the id of the category), post.ID in the post array within the Categories collection is IDENTICAL to the id of the post in Posts collection. This is the crux of it all, help me create a relationship so that, instead of displaying title or post.title from the post array within the Categories Collection, I need to display title.rendered within the original Posts Collection.

回答1:

it sounds like you don't control the data format, so you would have to use a nested loop to deal with the format you have. i think the following should work.

Template.userTimeline.helpers({
  categories: function() {
    return Category.find({
      id: {
        $in: Meteor.user().category
      }
    });
  }
});

note i changed the name of your helper.

{{#each category in categories}}
  {{category.description}}
  {{#each post in category.posts}}
    {{post.title}}
  {{/each}}
{{/each}}

c.f. http://blazejs.org/guide/spacebars.html#Each-in

EDIT:

ideally, you would be able to do a server-side join using a package like https://atmospherejs.com/reywood/publish-composite

but since you don't have control over that, you can do a client-side join. you would do this in the onCreated() of your template (caveat: this is untested!)

this.subscribe('posts', function() {
    let cursor = Categories.find({id: {$in: Meteor.user().category}});

    cursor.observe({
        added: function(newDocument) {
            let category = newDocument;

            // for this category, get all the associated post ids
            let postIds = _.map(category.posts, function(p)) {
                return p.ID;
            };

            // get the posts. we can fetch these because we waited for the publish of the posts collection.
            let actualPosts = Posts.find({id: {$in: postIds}}).fetch();

            // attach the actual posts to the category
            category.actualPosts = actualPosts;
        },
        changed: function(newDocument, oldDocument)  {
            // if wanting to track changes to the categories, similar to added block
        }
    });
});

then with a simple change to the template, you can grab those actual posts:

{{#each category in categories}}
  {{category.description}}
  {{#each post in category.actualPosts}}
    {{post.title}}
  {{/each}}
{{/each}}

doing the client-side joins is a little more tricky, because as the above is written, you're missing reactivity to the user's tracked categories, and posts w/in the existing categories. so you could further put all that in an autorun. that would track changes to Meteor.user().category, but not sure if that would track post changes w/in the categories. but this should get you a lot of the way there.

EDIT 2:

oh yeah, if you do it that way, that listener will stay active when the template goes away unless you call stop() on it. so...

Template.userTimeline.onDestroyed(function() {
    let handle = this.observeHandle.get();

    if (handle) {
        handle.stop();
    }
});

Template.userTimeline.onCreated(function() {
    let self = this;
    self.observeHandle = new ReactiveVar();

    self.subscribe('posts', function() {
        let cursor = Categories.find({id: {$in: Meteor.user().category}});

        let handle = cursor.observe({
            added: function(newDocument) {
                let category = newDocument;

                // for this category, get all the associated post ids
                let postIds = _.map(category.posts, function(p)) {
                    return p.ID;
                };

                // get the posts
                let actualPosts = Posts.find({id: {$in: postIds}}).fetch();

                // attach the actual posts to the category
                category.actualPosts = actualPosts;
            },
            changed: function(newDocument, oldDocument)  {
                // if wanting to track changes, similar to added block
            }
        });

        self.observeHandle.set(handle);
    });
});