Meteor, One to Many Relationship & add field only

2019-01-27 05:57发布

问题:

Can anyone see what may be wrong in this code, basically I want to check if a post has been shared by the current logged in user AND add a temporary field to the client side collection: isCurrentUserShared.

This works the 1st time when loading a new page and populating from existing Shares, or when adding OR removing a record to the Shares collection ONLY the very 1st time once the page is loaded.

1) isSharedByMe only changes state 1 time, then the callbacks still get called as per console.log, but isSharedByMe doesn't get updated in Posts collection after the 1st time I add or remove a record. It works the 1st time.

2) Why do the callbacks get called twice in a row, i.e. adding 1 record to Sharescollection triggers 2 calls, as show by console.log.

Meteor.publish('posts', function() {

    var self = this;
    var mySharedHandle;


    function checkSharedBy(IN_postId) {
        mySharedHandle = Shares.find( { postId: IN_postId, userId: self.userId }).observeChanges({

            added: function(id) {
                console.log("   ...INSIDE checkSharedBy(); ADDED: IN_postId = " + IN_postId );
                self.added('posts', IN_postId, { isSharedByMe: true });
            },

            removed: function(id) {
                console.log("   ...INSIDE checkSharedBy(); REMOVED: IN_postId = " + IN_postId );
                self.changed('posts', IN_postId, { isSharedByMe: false });
            }
        });
    }


    var handle = Posts.find().observeChanges({

        added: function(id, fields) {
            checkSharedBy(id);
            self.added('posts', id, fields);
        },

        // This callback never gets run, even when checkSharedBy() changes field isSharedByMe.
        changed: function(id, fields) {
            self.changed('posts', id, fields);
        },

        removed: function(id) {
            self.removed('posts', id);
        }
    });

    // Stop observing cursor when client unsubscribes
    self.onStop(function() {
        handle.stop();
        mySharedHandle.stop();
    });

    self.ready();
});

回答1:

Personally, I'd go about this a very different way, by using the $in operator, and keeping an array of postIds or shareIds in the records.

http://docs.mongodb.org/manual/reference/operator/query/in/

I find publish functions work the best when they're kept simple, like the following.

Meteor.publish('posts', function() {
    return Posts.find();
});
Meteor.publish('sharedPosts', function(postId) {
    var postRecord = Posts.findOne({_id: postId});
    return Shares.find{{_id: $in: postRecord.shares_array });
});


回答2:

I am not sure how far this gets you towards solving your actual problems but I will start with a few oddities in your code and the questions you ask.

1) You ask about a Phrases collection but the publish function would never publish anything to that collection as all added calls send to minimongo collection named 'posts'.

2) You ask about a 'Reposts' collection but none of the code uses that name either so it is not clear what you are referring to. Each element added to the 'Posts' collection though will create a new observer on the 'Shares' collection since it calls checkSharedId(). Each observer will try to add and change docs in the client's 'posts' collection.

3) Related to point 2, mySharedHandle.stop() will only stop the last observer created by checkSharedId() because the handle is overwritten every time checkSharedId() is run.

4) If your observer of 'Shares' finds a doc with IN_postId it tries to send a doc with that _id to the minimongo 'posts' collection. IN_postId is passed from your find on the 'Posts' collection with its observer also trying to send a different doc to the client's 'posts' collection. Which doc do you want on the client with that _id? Some of the errors you are seeing may be caused by Meteor's attempts to ignore duplicate added requests.

From all this I think you might be better breaking this into two publish functions, one for 'Posts' and one for 'Shares', to take advantage of meteors default behaviour publishing cursors. Any join could then be done on the client when necessary. For example:

//on server
Meteor.publish('posts', function(){
  return Posts.find();
});

Meteor.publish('shares', function(){
  return Shares.find( {userId: this.userId }, {fields: {postId: 1}} );
});

//on client - uses _.pluck from underscore package
Meteor.subscribe( 'posts' );
Meteor.subscribe( 'shares');

Template.post.isSharedByMe = function(){  //create the field isSharedByMe for a template to use
  var share = Shares.findOne( {postId: this._id} );
  return share && true;
};

Alternate method joining in publish with observeChanges. Untested code and it is not clear to me that it has much advantage over the simpler method above. So until the above breaks or becomes a performance bottleneck I would do it as above.

Meteor.publish("posts", function(){
  var self = this;
  var sharesHandle;
  var publishedPosts = [];
  var initialising = true;  //avoid starting and stopping Shares observer during initial publish

  //observer to watch published posts for changes in the Shares userId field
  var startSharesObserver = function(){
    var handle = Shares.find( {postId: {$in: publishedPosts}, userId === self.userId }).observeChanges({

      //other observer should have correctly set the initial value of isSharedByMe just before this observer starts.
      //removing this will send changes to all posts found every time a new posts is added or removed in the Posts collection 
      //underscore in the name means this is undocumented and likely to break or be removed at some point
      _suppress_initial: true,

      //other observer manages which posts are on client so this observer is only managing changes in the isSharedByMe field 
      added: function( id ){
        self.changed( "posts", id, {isSharedByMe: true} );
      },

      removed: function( id ){
        self.changed( "posts", id, {isSharedByMe: false} );
      }
    });
    return handle;
  };

  //observer to send initial data and always initiate new published post with the correct isSharedByMe field.
  //observer also maintains publishedPosts array so Shares observer is always watching the correct set of posts.  
  //Shares observer starts and stops each time the publishedPosts array changes
  var postsHandle = Posts.find({}).observeChanges({
    added: function(id, doc){
      if ( sharesHandle ) 
        sharesHandle.stop();
      var shared = Shares.findOne( {postId: id});
      doc.isSharedByMe = shared && shared.userId === self.userId;
      self.added( "posts", id, doc);
      publishedPosts.push( id );
      if (! initialising)
        sharesHandle = startSharesObserver();
    },
    removed: function(id){
      if ( sharesHandle ) 
        sharesHandle.stop();
      publishedPosts.splice( publishedPosts.indexOf( id ), 1);
      self.removed( "posts", id );
      if (! initialising)
        sharesHandle = startSharesObserver();
    },
    changed: function(id, doc){
      self.changed( "posts", id, doc);
    }
  });

  if ( initialising )
    sharesHandle = startSharesObserver();

  initialising = false;
  self.ready();

  self.onStop( function(){
    postsHandle.stop();
    sharesHandle.stop();
  });
});


回答3:

myPosts is a cursor, so when you invoke forEach on it, it cycles through the results, adding the field that you want but ending up at the end of the results list. Thus, when you return myPosts, there's nothing left to cycle through, so fetch() would yield an empty array.

You should be able to correct this by just adding myPosts.cursor_pos = 0; before you return, thereby returning the cursor to the beginning of the results.