Master Many-to-Many Associations with ActiveRecord

Mô hình hóa những mối quan hệ many-to-many giữa các thực thể dữ liệu trong ActiveRecord không phải lúc nào cũng là một nhiệm vụ dễ dàng. Thậm chí, kể cả khi chúng ta đã thiết kế sẵn một ER diagram(mô hình thực thể liên kết) để làm việc, nó không phải lúc nào cũng rõ ràng với những quan hệ sử dụng ActiveRecord. Có 2 kiểu quan hệ many-to-many: transitive và intransitive. Trong toán học thì:

  • Một quan hệ R là một binary relation trên tập hợp A là một quy tắc, sao cho với mỗi cặp (x,y), x,y bất kì thuộc A thì câu lệnh xRy sẽ trả về một trong 2 giá trị: true hoặc false. Ví dụ phép toán "<" là một binary relation trong các tập hợp: ℕ, ℤ, ℝ..

  • Trong những binary relation trên có những quan hệ có thể móc nối với nhau. Một binary relation R trên tập A được gọi là transitive nếu với mỗi cặp x,y,z ∈ A, nếu xRy và yRz thì xRz.

Đặt nó vào trong bối cảnh của các dữ liệu Model, một quan hệ giữa hai thực thể là transitive nếu nó có thể được diễn tả tốt nhất thông qua một hay nhiều thực thể khác. Cho ví dụ, thật dễ dàng để nhìn thấy rằng: một Buyer mua hàng từ nhiều Sellers trong khi một Seller bán hàng đến nhiều Buyers. Tuy nhiên, mối quan hệ sẽ không được diễn tả một cách đầy đủ khi chúng ta thêm những thực thể khác như Product, Payment, Marketplace... Các mối quan hệ như vậy có thể coi như một kiểu many-to-many transitive vì chúng ta hiểu được rằng, sự hiện diện của các thực thể khác sẽ tóm được đầy đủ ý nghĩa của mối quan hệ này. May mắn thay, ActiveRecord cho phép chúng ta mô hình hóa các mối quan hệ như vậy một cách dễ dàng. Chúng ta sẽ bắt đầu tìm hiểu các liên kết many-to-many trong ActiveRecord và cách thức chúng làm việc.

I, Intransitive Associations

Đây là liên kết many-to-many đơn giản nhất. Hai models được kết nối với nhau bằng chính ý nghĩa đơn giản nhất cho sự tồn tại của chúng. Một Book có thể được viết bằng nhiều authors và một Author có thể viết nhiều books. Nó là một liên kết trực tiếp và có sự phụ thuộc trực tiếp giữa 2 model này. Dường như chúng ta không thể có sự lựa chọn khác ngoài chúng. Trong ActiveRecord, việc này thật dễ dàng để mô hình hóa với liên kết has_and_belongs_to_many (HABTM). Chúng ta có thể tạo các models và migrations cho quan hệ này trong Rails bằng các lệnh dưới đây:

    rails g model Author name:string
    rails g model Book title:string
    rails g migration CreateJoinTableAuthorsBooks authors books

Chúng ta cần định nghĩa liên kết HABTM trong các models của chúng ta như sau:

    class Book < ApplicationRecord
      has_and_belongs_to_many :authors
    end
    class Author < ApplicationRecord
      has_and_belongs_to_many :books
    end

Rùi tiến hành tạo bảng:

    rake db:migrate

Cuối cùng, ta có thể populate database:

    herman = Author.create name: 'Herman Melville'
    moby = Book.create title: 'Moby Dick'
    herman.books << moby

Rất dễ dàng!

II, Mono-transitive Associations Một liên kết transitive, cái mà có thể được miêu tả tốt nhất bằng việc add thêm một model phụ. Cho ví dụ: một Student có thể được dạy bởi nhiều Tutors và một Tutor có thể dạy nhiều Students, nhưng chúng ta không thể diễn tả đầy đủ mối quan hệ này nếu chúng ta không add thêm một thực thể khác, là Class(để tránh nhầm lẫn với từ khóa trong Ruby, ta đặt tên nó là Klass)

    rails g model Student name:string
    rails g model Tutor name:string
    rails g model Klass subject:string student:references tutor:references

Chúng ta có thể nói rằng: một Student được dạy thông qua việc tham dự Klasses và ngược lại, một Tutor dạy những Students thông qua các Klasses. Từ khóa "through" rất quan trọng ở đây, vì chúng ta sẽ sử dụng nó trong ActiveRecord để định nghĩa kiểu liên kết này:

    class Student < ApplicationRecord
      has_many :klasses
      has_many :tutors, through: :klasses
    end
    class Tutor < ApplicationRecord
      has_many :klasses
      has_many :students, through: :klasses
    end
    class Klass < ApplicationRecord
      belongs_to :student
      belongs_to :tutor
    end

Sau đó, tạo database bằng lệnh:

    rake db:migrate

Giờ, chúng ta có thể populate database:

    bart = Student.create name: 'Bart Simpson'
    edna = Tutor.create name: 'Mrs Krabapple'
    Klass.create subject: 'Maths', student: bart, tutor: edna

Với câu lệnh find thông thường, chúng ta có thể tạo một vài câu lệnh truy vấn:

    Student.find_by(name: 'Bart Simpson').tutors  # find all Bart's tutors
    Student.joins(:klasses).where(klasses: {subject: 'Maths'}).distinct.pluck(:name) # get all students who attend the Maths class
    Student.joins(:tutors).joins(:klasses).where(klasses: {subject: 'Maths'}, tutors: {name: 'Mrs Krabapple'}).distinct.map {|x| puts x.name} # get all students who attend Maths taught by Mrs Krabapple

Trong hầu hết các trường hợp của liên kết mono-transitive, tên của các model phản ánh ý nghĩa của liên kết(ví dụ như X has_many Z through Y), chúng ta không cần làm gì nữa và ActiveRecord đã mô hình hóa các liên kết một cách hoàn hảo.

III, Multi-transitive Associations Một liên kết multi-transitive là một liên kết, cái mà có thể được diễn tả đầy đủ thông qua nhiều models khác. Như Developers chẳng hạn, Developers được liên kết với nhiều software Community. Liên kết của chúng ta, thông qua nhiều form: chúng ta có thể contribute code, post tại một forum, tham dự các events và nhiều cách khác. Mỗi Developer được liên kết với Commuity thông qua các action xác định. Giả sử chúng ta chọn 3 actions làm ví dụ sau:

  • Contributing code

  • Posting on forums

  • Attending events Bước tiếp theo trong tiến trình mô hình hóa của chúng ta là định nghĩa các thực thể dữ liệu(models) cái sẽ giúp nhận ra thông qua các actions. Với vi dụ trên, chúng ta có thể tiến hành khai báo:

                  | Association          | through Model |
                  |--------------------------------------|
                  | contributing code    | Repository    | 
                  |--------------------------------------|
                  | posting at forums    | Forum         |
                  |--------------------------------------|
                  | attending events     | Event         |
    

Sau đó, tạo các model cần thiết:

    rails g model Community name:string
    rails g model Developer name:string
    rails g model Repo url:string comment:string developer:references community:references
    rails g model Forum url:string post:text developer:references community:references
    rails g model Event location:string name:string developer:references community:references
    rails db:migrate

Tạo một vài giá trị Developers và Communities:

    devs = %w(joe sue fred mary).map {|dev| Developer.create name: dev}
    comms = %w(rails nosql javascript postgres).map {|comm| Community.create name: comm}

Sau đó, chúng ta có thể định nghĩa các liên kết giữa các models. Tại thời điểm này chúng ta sử dụng vài technique trong mono-transitive và nhắc lại has_many...through trong mỗi liên kết:

    class Developer < ApplicationRecord
      has_many :events
      has_many :forums
      has_many :repos
      has_many :appearances, through: :events  #FAIL
      has_many :postings, through: :forums #FAIL
      has_many :contributions, through: :repos #FAIL
    end

Tuy nhiên, nó sẽ không làm việc vì ActiveRecord sẽ cố gắng infer tên của các source model từ tên các liên kết và nó sẽ fail. Để giải quyết chúng, ta cần xác định tên của source model bằng cách sử dụng option :source

    class Developer < ApplicationRecord
      has_many :events
      has_many :forums
      has_many :repos
      has_many :appearances, through: :events, source: :community
      has_many :postings, through: :forums, source: :community
      has_many :contributions, through: :repos, source: :community
    end

Tương tự với Communities:

    class Community < ApplicationRecord
      has_many :events
      has_many :forums
      has_many :repos
      has_many :hostings, through: :events, source: :developer
      has_many :discussions, through: :forums, source: :developer
      has_many :contributions, through: :repos, source: :developer
    end

Như chúng ta thấy, trên model Community, chúng ta đang thay đổi tên của một vài liên kết để diễn tả một cách tự nhiên từ nhiều phía của quan hệ, Ví dụ: một Developer tạo các appearances tại events, trong khi Community hosts events. Một Developer post tại các forum trong khi a Community fosters discussions tại forum. Bằng cách này, chúng ta chắc chắn rằng tên của các method mang ý nghĩa rõ ràng.

Giờ chúng ta có thể tạo một vài event, repo, forum...

    Repo.create url: 'www.gitlab.com/342', comment: 'ruby code', developer_id: devs[0].id, community_id: comms[0].id
    Repo.create url: 'www.gitlab.com/662', comment: 'callbacks sample', developer_id: devs[0].id, community_id: comms[2].id
    Repo.create url: 'www.jsfiddle.com/abcg3', comment: 'reactive sample', developer_id: devs[1].id, community_id: comms[3].id
    Repo.create url: 'www.jsfiddle.com/563', comment: 'promises sample', developer_id: devs[2].id, community_id: comms[3].id
    Forum.create url: 'www.stackoverflow.com/mongodb', post: 'this is what I think...', developer_id: devs[2].id, community_id: comms[1].id
    Forum.create url: 'www.redis.com/563', post: 'my opinion is...', developer_id: devs[3].id, community_id: comms[1].id
    Event.create location: 'Bath, UK', name: 'Bath Ruby', developer_id: devs[2].id, community_id: comms[0].id
    Event.create location: 'Tech Institute', name: 'London NoSQL Meetup', developer_id: devs[2].id, community_id: comms[1].id

Giờ thì chúng ta có thể thực hiện các truy vấn từ các models trên:

    devs.find_by(name: 'fred').appearances # events a developer has appeared at
    Event.find_by(community: comms[0]) # all events for the Rails community
    Forum.where(developer: Developer.find_by(name: 'fred') # all forums where a specific developer has posted
    Community.find_by(name: 'rails').hostings + Community.find_by(name: 'rails').discussions + Community.find_by(name: 'rails').contributions # get all events, forums and repositories for a specific community
    Developer.select('distinct developers.name').joins(:repos).joins(:events).joins(:forums) # find developers who have appeared in Events, contributed to Repos and chatted on Forums, for any Community

IV, The Gist

  • Nếu bạn có một quan hệ many-to-many trực tiếp giữa 2 modes, không cần làm rõ thêm ý nghĩa để miêu tả quan hệ, hãy sử dụng HABTM: has_and_belongs_to_many.
  • Nếu quan hệ many-to-many không trực tiếp hay cần một thực thể phụ để diễn tả đẩy đủ mối quan hệ, và tên mối quan hệ có thể được tóm bởi tên của model phụ, hãy sử dụng has_many :through.
  • Nếu quan hệ many-to-many có nhiều sắc thái, cái mà yêu cầu nhiều thực thể khác để diễn tả chúng, hay sử dụng has_many :through :source.

Tóm lại, việc mô hình hóa các mối quan hệ many-to-many sử dụng ActiveRecord có thể là thách thức không nhỏ, tuy nhiên, một khi bạn hiểu được ý nghĩa tự nhiên của các liên kết và những option linh hoạt mà ActiveRecord cung cấp, nó sẽ trở nên dễ dàng!

Nguồn: https://www.sitepoint.com/master-many-to-many-associations-with-activerecord/ https://web.stanford.edu/class/archive/cs/cs103/cs103.1142/lectures/07/Small07.pdf