Publications ขั้นสูง

บทแทรก 13.5

แปลไปแล้ว

ในบทนี้ คุณจะได้

  • เรียนรู้รูปแบบวิธีจัดการเผยแพร่ข้อมูลแบบก้าวหน้า
  • เข้าใจความยืดหยุ่นของการเผยแพร่และบอกรับข้อมูล
  • ถึงตอนนี้คุณน่าจะมีความเข้าใจการทำงานระหว่างการเผยแพร่และการบอกรับข้อมูลเป็นอย่างดีแล้ว ดังนั้นเราจะไม่มาฝึกฝนอะไรอีก แต่จะมาทำความเข้าใจวิธีการที่ก้าวหน้ามากขึ้นซักสองสามกรณี

    เผยแพร่คอลเลกชั่นหลายครั้ง

    ใน บทแทรกที่เกี่ยวกับการเผยแพร่ข้อมูล เราได้เห็นรูปแบบทั่วไปของการเผยแพร่และบอกรับข้อมูลกันมาแล้ว และเราก็รู้ว่าฟังก์ชัน _publishCursor ช่วยให้เราใช้งานมันได้ง่ายแค่ไหนกับไซต์ของเรา

    ก่อนอื่นเรามาทบทวนว่า _publishCursor ทำอะไรให้เรา สิ่งที่มันทำก็คือ หาเอกสารทั้งหมดที่ตรงกับเคอร์เซอร์ที่ขอมา และส่งพวกมันกลับมาให้คอลเลกชั่นที่ฝั่งไคลเอนต์ ด้วยชื่อเดียวกัน สังเกตุว่า ชื่อของ การเผยแพร่ ไม่ได้ถูกนำมาใช้

    นั่นก็หมายความว่า เราสามารถมีได้ มากกว่าหนึ่งการเผยแพร่ ที่เชื่อมระหว่างไคลเอนต์ กับ คอลเลกชั่นเวอร์ชันต่างๆของเซฺิร์ฟเวอร์

    เรารู้จักรูปแบบนี้กันมาแล้วใน บทการแบ่งหน้า เมื่อเราเผยแพร่หน้าย่อยๆ ของข่าวทั้งหมดเพิ่มเติมจากข่าวที่กำลังแสดงอยู่

    อีกกรณีที่มีการใช้งานคล้ายๆกัน คือ การเผยแพร่ ภาพรวม ของชุดเอกสารขนาดใหญ่ ควบคู่กับรายละเอียดทั้งหมดของเอกสารตัวเดียว

    Publishing a collection twice
    Publishing a collection twice
    Meteor.publish('allPosts', function() {
      return Posts.find({}, {fields: {title: true, author: true}});
    });
    
    Meteor.publish('postDetail', function(postId) {
      return Posts.find(postId);
    });
    

    เมื่อไคลเอนต์บอกรับข้อมูลจากการเผยแพร่ทั้งสอง คอลเลกชั่น posts ของมันก็ถูกสร้างจากแหล่งข้อมูลสองตัวด้วย คือ รายการข่าวที่มีชื่อข่าวและชื่อผู้แต่ง ได้ข้อมูลจากการบอกรับตัวแรก กับข่าวที่มีรายละเอียดอีกหนึ่งข่าว ที่ได้ข้อมูลมาจากตัวที่สอง

    คุณอาจจะมองออกว่า ข่าวที่ถูกเผยแพร่โดย postDetail ก็ถูกเผยแพร่โดย allPosts ด้วยเช่นกัน (แม้ว่ามันจะมีข้อมูลแค่บางส่วนเท่านั้น) ซึ่ง Meteor จะจัดการข้อมูลส่วนที่ทับซ้อนนี้ให้เรา โดยนำฟิลด์มารวมกัน และป้องกันไม่ให้มีข่าวซ้ำ

    วิธีนี้เยี่ยมไปเลย เพราะในตอนที่เราสร้างรายการข่าวแบบสรุปนั้น เรากำลังใช้ข้อมูลจากอ็อบเจ็กต์ที่มีข้อมูลแค่เพียงพอให้เราใช้แสดงผลเท่านั้น ส่วนในตอนที่เราสร้างหน้าข่าวเดี่ยวๆนั้น เรามีข้อมูลทั้งหมดที่เราเอามาแสดงผลได้ และก็แน่นอนที่เราต้องคอยระวังว่า ข้อมูลที่ไคลเอนต์ไม่ได้มีฟิลด์ทั้งหมดติดมากับทุกข่าว ซึ่งในกรณีนี้อาจทำให้เกิดบั๊กได้ง่ายๆ

    สิ่งที่ต้องจำไว้คือ ไม่มีข้อจำกัดว่า คุณต้องเผยแพร่ข้อมูลที่แตกต่างกันไปในแต่ละครั้ง คุณอาจเผยแพร่ข้อมูลชุดเดียวกันทั้งสองครั้งของการเผยแพร่ แต่จัดลำดับข้อมูลต่างกันก็ได้

    Meteor.publish('newPosts', function(limit) {
      return Posts.find({}, {sort: {submitted: -1}, limit: limit});
    });
    
    Meteor.publish('bestPosts', function(limit) {
      return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
    });
    
    server/publications.js

    เผยแพร่ตัวเดียว บอกรับหลายครั้ง

    เราเพิ่งจะเห็นวิธีการที่คุณสามารถเผยแพร่คอลเลกชั่นตัวเดียวได้มากกว่าหนึ่งครั้ง และที่น่าแปลกใจคือ คุณก็สามารถทำให้เกิดผลลัพธ์คล้ายกันด้วยวิธีการอีกแบบได้ ด้วยการสร้างการเผยแพร่แค่ตัวเดียว แต่ บอกรับข้อมูล กับมัน หลายๆครั้ง

    ใน Microscope เราบอกรับการเผยแพร่ posts หลายครั้ง แต่ Iron Router จะสร้างและลบการบอกรับแต่ละตัวให้เรา และก็ไม่มีเหตุผลว่า ทำไมเราถึงไม่สามารถบอกรับข้อมูลหลายๆครั้ง พร้อมๆกัน ได้

    จากตัวอย่าง สมมุติว่าเราต้องการโหลดทั้งข่าวใหม่สุดและข่าวดีที่สุด มาไว้ที่หน่วยความจำในเวลาเดียวกัน

    Subscribing twice to one publication
    Subscribing twice to one publication

    เราก็สร้างการเผยแพร่ขึ้นมาหนึ่งตัว

    Meteor.publish('posts', function(options) {
      return Posts.find({}, options);
    });
    

    แล้วเราก็บอกรับการเผยแพร่นี้หลายครั้ง จริงๆแล้วก็ไม่แตกต่างจากที่เราทำใน Microscope

    Meteor.subscribe('posts', {submitted: -1, limit: 10});
    Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
    

    จริงๆแล้วมันเกิดอะไรขึ้นกันแน่ คำตอบคือ เบราว์เซอร์แต่ละตัวจะเปิดใช้ การบอกรับ สองตัว ที่แตกต่างกัน โดยแต่ละตัวเชื่อมต่อไปที่การเผยแพร่ ตัวเดียวกัน

    การบอกรับแต่ละตัวส่งค่าอาร์กิวเมนท์ที่แตกต่างกันไปให้การเผยแพร่ ซึ่งโดยพื้นฐานแล้ว ในแต่ละครั้ง ชุดเอกสาร (ที่แตกต่างกัน) จะถูกดึงออกมาจากคอลเลกชั่น posts และส่งกลับมาที่คอลเลกชั่นฝั่งไคลเอนต์

    นอกจากนี้ คุณยังสามารถบอกรับไปที่การเผยแพร่ตัวเดียวทั้งสองครั้งด้วย อาร์กิวเมนท์ชุดเดียวกันก็ได้ ! ซึ่งวิธีใช้แบบนั้นอาจจะหาวิธีทำให้เกิดประโยชน์ได้ยาก แต่ด้วยความยืดหยุ่นที่มี เราอาจมีโอกาสนำไปใช้ประโยชน์ได้ในวันหน้า !

    คอลเลกชั่นหลายตัว บอกรับครั้งเดียว

    สิ่งที่ไม่เหมือนกับฐานข้อมูลแบบ relational เช่น MySQL ที่ใช้การ joins เป็นหลัก ก็คือ ฐานข้อมูลแบบ NoSQL เช่น Mongo จะเกี่ยวข้องกับ denormalizing และ embedding เป็นสำคัญ เราจะมาดูกันว่า มันทำงานได้อย่างไรใน Meteor

    ลองดูตัวอย่างที่ชัดเจนกันก่อน การที่เราเพิ่มข้อคิดเห็นเข้าไปที่ข่าว และเราก็พอใจกับการเผยแพร่เฉพาะข้อคิดเห็นของข่าวที่ผู้ใช้กำลังดูอยู่เท่านั้น

    แต่ถ้าหากว่า เราต้องการจะแสดงข้อคิดเห็นของข่าว ทั้งหมด ในหน้าแรกขึ้นมาล่ะ (จำไว้ด้วยว่าข่าวที่แสดงจะเปลี่ยนไปเมื่อเราโหลดข่าวที่เหลือเพิ่มเข้ามาอีก ด้วยวิธีแบ่งหน้า) กรณีนี้ทำให้เรามีเหตุผลที่ดีที่จะฝังข้อคิดเห็นเข้าไปในข่าว และจริงๆแล้วมันคือสาเหตุหนึ่งที่เราทำ denormalize กับ จำนวน ข้อคิิดเห็นด้วย

    อันที่จริงเราก็สามารถฝังข้อคิดเห็นเข้าไปในข่าว และกำจัดคอลเลกชั่น Comments ออกไปพร้อมๆกันได้อยู่แล้ว แต่ก็เหมือนกับที่เราเห็นในบท การ denormalize ที่การทำอย่างนั้น จะทำให้เราสูญเสียความสามารถพิเศษที่จะใช้จัดการกับคอลเลกชั่นที่แยกออกจากกัน

    แต่ปรากฏว่า มันยังมีวิธีที่เกี่ยวข้องกับการบอกรับข้อมูล ที่ทำให้สามารถฝังข้อคิดเห็นของเรา และแยกคอลเลกชั่นออกจากกันได้ด้วย

    สมมุติว่า ในหน้าแรกที่เราแสดงรายการข่าวนั้น เราต้องการให้แต่ละข่าว บอกรับข้อมูลข้อคิดเห็นสองตัวแรก ไปพร้อมๆกัน

    มันค่อนข้างยากที่จะทำแบบนี้ได้ด้วยการใช้ข้อมูลจากการเผยแพร่ที่ไม่เกี่ยวข้องกัน โดยเฉพาะอย่างยิ่งเมื่อจำนวนข่าวถูกจำกัดไว้ด้วยเงื่อนไข (เช่น 10 ข่าวล่าสุด) ซึ่งทำให้เราต้องเขียนโค้ดการเผยแพร่คล้ายๆแบบนี้

    Two collections in one subscription
    Two collections in one subscription
    Meteor.publish('topComments', function(topPostIds) {
      return Comments.find({postId: topPostIds});
    });
    

    ซึ่งจะทำให้เกิดปัญหาต่อประสิทธิภาพแน่ เพราะการเผยแพร่จะถูกลบทิ้งและสร้างใหม่ทุกครั้งที่ topPostIds เปลี่ยนไป

    แต่ก็ยังมีวิธีแก้ โดยใช้ความจริงที่ว่า เราไม่เพียงแค่สามารถมี การเผยแพร่ มากกว่าหนึ่งตัว ต่อ คอลเลกชั่น แต่เรายังมีได้มากกว่าหนึ่ง คอลเลกชั่น ต่อ การเผยแพร่ ได้ด้วย

    Meteor.publish('topPosts', function(limit) {
      var sub = this, commentHandles = [], postHandle = null;
    
      // send over the top two comments attached to a single post
      function publishPostComments(postId) {
        var commentsCursor = Comments.find({postId: postId}, {limit: 2});
        commentHandles[postId] = 
          Mongo.Collection._publishCursor(commentsCursor, sub, 'comments');
      }
    
      postHandle = Posts.find({}, {limit: limit}).observeChanges({
        added: function(id, post) {
          publishPostComments(id);
          sub.added('posts', id, post);
        },
        changed: function(id, fields) {
          sub.changed('posts', id, fields);
        },
        removed: function(id) {
          // stop observing changes on the post's comments
          commentHandles[id] && commentHandles[id].stop();
          // delete the post
          sub.removed('posts', id);
        }
      });
    
      sub.ready();
    
      // make sure we clean everything up (note `_publishCursor`
      //   does this for us with the comment observers)
      sub.onStop(function() { postHandle.stop(); });
    });
    

    จะเห็นว่าเราไม่ได้คืนค่าอะไรออกมาจากการเผยแพร่เลย เพราะเราส่งข้อความให้ sub ด้วยตัวเอง (ผ่าน .added() และเพื่อนๆ) ดังนั้นเราก็ไม่จำเป็นต้องใช้ _publishingCursor ทำงานให้เราโดยคืนค่าเคอร์เซอร์กลับมา

    ตอนนี้ทุกๆครั้งที่เราเผยแพร่ข่าว เราก็เผยแพร่ข้อคิดเห็นสองตัวแรกติดไปกับมันด้วย โดยใช้การบอกรับแค่ครั้งเดียว !

    ถึงแม้ว่า Meteor จะไม่ได้ทำให้วิธีนี้ใช้งานได้โดยตรง แต่คุณก็สามารถดูได้ที่แพ็คเกจ publish-with-relationsบนเว็บ Atmosphere ได้ ซึ่งแพ็คเกจนี้จะช่วยให้เราใช้งานในรูปแบบนี้ได้ง่ายขึ้น

    เชื่อมโยงหลายคอลเลกชั่น

    ด้วยความยืดหยุ่นของการบอกรับข้อมูลที่เราเพิ่งค้นพบนั้น คำตอบคือ ถ้าเราไม่ใช้ _publishCursor เราก็ไม่จำเป็นต้องทำตามข้อจำกัดที่ว่า คอลเลกชั่นต้นทางบนเซิร์ฟเวอร์จำเป็นต้องมีชื่อเดียวกับคอลเลกชั่นปลายทางที่ไคลเอนต์

    One collection for two subscriptions
    One collection for two subscriptions

    เหตุผลหนึ่งที่ทำให้คุณต้องทำสิ่งนี้คือ การสืบทอดจากตารางเดียว (SIngle Table Inheritance)

    สมมุติว่าเราต้องการอ้างถึงอ็อบเจกต์ชนิดต่างๆของข่าวเรา แต่ละตัวถูกเก็บในฟิลด์ที่ใช้ร่วมกัน โดยมีข้อมูลแตกต่างกันเล็กน้อย อย่างเช่น เราอาจสร้างระบบทำบล็อกคล้ายๆ Tumblr ที่แต่ละข่าวมี ID เวลา และชื่อ และยังสามารถมี ภาพ วีดีโอ ลิงก์ หรือแค่ข้อความ ใส่เข้าไปได้

    เราสามารถที่จะเก็บอ็อบเจกต์เหล่านี้ลงในคอลเลกชั่น 'resources' ตัวเดียวได้ และใช้แอททริบิวต์ type เป็นตัวแยกแยะประเภทของมัน (ว่าเป็น video image link หรืออื่นๆ)

    แม้เราจะมีแค่คอลเลกชั่น Resources ตัวเดียวบนเซิร์ฟเวอร์ เราก็สามารถแปลงคอลเลกชั่นตัวเดียวนั้นให้กลายเป็นคอลเลกชั่นของ Videos ของ Images และของตัวอื่นๆได้ ด้วยความพิเศษของโค้ดดังนี้

      Meteor.publish('videos', function() {
        var sub = this;
    
        var videosCursor = Resources.find({type: 'video'});
        Mongo.Collection._publishCursor(videosCursor, sub, 'videos');
    
        // _publishCursor doesn't call this for us in case we do this more than once.
        sub.ready();
      });
    

    เราบอกให้ _publishCursor เผยแพร่วีดีโอของเรา (คืนค่ากลับมา) เหมือนที่ทำตามปกติ แต่แทนที่เราจะเผยแพร่ไปที่คอลเลกชั่น resources บนไคลเอนต์ เราก็จะเผยแพร่จาก 'resources' ไปที่ 'videos' แทน

    อีกแนวคิดนึงที่คล้ายๆกันคือ ทำการเผยแพร่ไปที่คอลเลกชั่นฝั่งไคลเอนต์ โดย ไม่ต้องมีคอลเลกชั่นที่เซิร์ฟเวอร์เลย ! อย่างเช่น คุณอาจจะดึงข้อมูลจากบริการของบุคคลที่สาม แล้วเผยแพร่มันไปที่คอลเลกชั่นฝั่งไคลเอนต์

    ก็ต้องขอบคุณความยืดหยุ่นของ API ของการเผยแพร่ ที่ทำให้ความเป็นไปได้ไม่มีที่สิ้นสุด