ירושה של שולחן יחיד לעומת אסוציאציות פולימורפיות ב- Rails: מצא מה מתאים לך

אם אי פעם יצרת יישום עם יותר ממודל אחד, היית צריך לחשוב באיזה סוג מערכות יחסים להשתמש בין המודלים האלה.

ככל שמורכבות היישום הולכת וגוברת, זה יכול להיות קשה להחליט אילו מערכות יחסים צריכות להתקיים בין המודלים שלך.

מצב המופיע לעיתים קרובות הוא כאשר כמה מהדגמים שלך צריכים לקבל גישה לפונקציונליות של דגם שלישי. שתי שיטות Rails נותן לנו להתמודד עם האירוע הזה הן ירושה יחיד שולחן ו עמותה פולימורפיים.

בתורשה של שולחן יחיד (STI), מחלקות משנה רבות יורשות ממעמד-על אחד עם כל הנתונים באותה טבלה במסד הנתונים. לסופר-קלאס יש עמודת "סוג" כדי לקבוע לאיזה מחלקה-משנה אובייקט שייך.

In a עמותה פולימורפיים , מודל אחד "שייך" כמה דגמים אחרים באמצעות עמותה אחת. לכל דגם, כולל המודל הפולימורפי, טבלה משלו במסד הנתונים.

בואו נסתכל על כל שיטה כדי לראות מתי היינו משתמשים בהן.

ירושה של שולחן יחיד

דרך נהדרת לדעת מתי STI מתאים היא כאשר המודלים שלך יש נתונים / מצב משותף . התנהגות משותפת היא אופציונלית.

בואו נעמיד פנים שאנחנו יוצרים אפליקציה שמפרטת רכבים שונים העומדים למכירה בסוכנות מקומית. סוכנות זו מוכרת מכוניות, אופנועים ואופניים.

(אני יודע שסוכנויות לא מוכרות אופניים, אבל סחב איתי לרגע - תראה לאן אני הולך עם זה).

עבור כל רכב, הסוכנות רוצה לעקוב אחר המחיר, הצבע והאם הרכב נרכש. מצב זה הוא מועמד מושלם ל- STI, מכיוון שאנו משתמשים באותם נתונים עבור כל כיתה.

אנו יכולים ליצור מעמד על Vehicleעם התכונות של צבע, מחיר ונרכש. כל אחת משתי הקבוצות שלנו יכולה לרשת Vehicleמכולן ולהשיג את אותן התכונות בבת אחת.

ההגירה שלנו ליצירת טבלת הרכבים עשויה להיראות כך:

class CreateVehicles < ActiveRecord::Migration[5.1] def change create_table :vehicles do |t| t.string :type, null: false t.string :color t.integer :price t.boolean :purchased, default: false end end end

חשוב שניצור את typeהעמודה למעמד העל. זה אומר ל- Rails שאנחנו משתמשים ב- STI ורוצים שכל הנתונים עבורם Vehicleושיעורי המשנה שלו יהיו באותה הטבלה במסד הנתונים.

שיעורי המודל שלנו ייראו כך:

class Vehicle < ApplicationRecordend
class Bicycle < Vehicleend
class Motorcycle < Vehicleend
class Car < Vehicleend

התקנה זו נהדרת מכיוון שכל שיטה או אימות Vehicleבכיתה משותפים עם כל אחת מתתי קבוצות המשנה שלה. אנו יכולים להוסיף שיטות ייחודיות לכל אחת משתי הקטעים לפי הצורך. הם בלתי תלויים זה בזה והתנהגותם אינה משותפת לרוחב.

בנוסף, מכיוון שאנו יודעים כי מחלקות המשנה חולקות את אותם שדות נתונים, אנו יכולים לבצע את אותן שיחות על אובייקטים ממחלקות שונות:

mustang = Car.new(price: 50000, color: red)harley = Motorcycle.new(price: 30000, color: black)
mustang.price=> 50000
harley.price=> 30000

הוספת פונקציונליות

בואו נגיד שהסוחר מחליט לאסוף מידע נוסף על הרכבים.

שכן Bicycles, היא רוצה לדעת אם כל אופניים הם דרך, הר או אופניים היברידיים. ובשביל Carsו Motorcycles, היא רוצה לעקוב אחר כוחות סוס.

אז אנו יוצרים הגירה להוסיף bicycle_typeו horsepowerאל Vehiclesהשולחן.

לפתע, הדגמים שלנו כבר לא חולקים שדות נתונים באופן מושלם. לכל Bicycleאובייקט לא תהיה horsepowerתכונה, ולכל Carאו Motorcycleלא יהיה bicycle_type(אני מקווה - אגיע לזה עוד רגע).

ובכל זאת לכל אופניים בשולחן שלנו יהיה horsepowerשדה, ולכל מכונית ואופנוע יהיה bicycle_typeשדה.

זה המקום בו הדברים יכולים להיות דביקים. כמה בעיות יכולות להתעורר במצב זה:

  1. לטבלה שלנו יהיו הרבה ערכי null ( nilבמקרה של רובי) מכיוון שלאובייקטים יהיו שדות שאינם חלים עליהם. אלה nullsעלולים לגרום לבעיות כאשר אנו מוסיפים אימות למודלים שלנו.
  2. ככל שהטבלה גדלה, אנו יכולים להיתקל בעלויות ביצועים בעת שאילתות אם לא נוסיף פילטרים. חיפוש אחר מסוים bicycle_typeיסתכל כל פריט ב שמונח על השולחן כך לא רק Bicycles, אבל Carsו Motorcyclesגם.
  3. כמו כן, אין שום דבר שעוצר בפני משתמש להוסיף נתונים "לא הולמים" למודל הלא נכון. לדוגמה, משתמש עם ידע כלשהו יכול ליצור א ' Bicycleעם horsepower100. נצטרך אימות ותכנון אפליקציות טוב כדי למנוע יצירת אובייקט לא חוקי.

לכן, כפי שאנו רואים, ל- STI יש כמה פגמים. זה נהדר עבור יישומים שבהם הדגמים שלך חולקים שדות נתונים ואינם צפויים להשתנות.

STI PROS:

  • פשוט ליישום
  • DRY - שומר קוד משוכפל באמצעות ירושה ותכונות משותפות
  • מאפשר לקבוצות משנה להתנהגות משלו לפי הצורך

STI CONS:

  • לא משתנה היטב: ככל שהנתונים גדלים, הטבלה יכולה להיות גדולה ואולי קשה לתחזוק / שאילתה
  • דורש טיפול בעת הוספת דגמים חדשים או שדות מודל החורגים מהשדות המשותפים
  • (מותנה) מאפשר יצירת אובייקטים לא חוקיים אם אימות אינו קיים
  • (מותנה) יכול להיות קשה לאמת או לשאול אם קיימים ערכי null רבים בטבלה

אגודות פולימורפיות

עם אסוציאציות פולימורפיות, מודל יכול belong_toמספר דגמים עם אסוציאציה אחת.

זה שימושי כאשר למספר מודלים אין קשר או חולקים נתונים זה עם זה, אלא יש קשר עם המעמד הפולימורפי.

לדוגמא, בואו נחשוב על אתר מדיה חברתית כמו פייסבוק. בפייסבוק, אנשים פרטיים וקבוצות יכולים לשתף פוסטים.

האנשים והקבוצות אינם קשורים זה לזה (מלבד שניהם סוג של משתמש), ולכן יש להם נתונים שונים. יש קבוצה כנראה שדות כמו member_countואת group_typeזה אינו חל על אדם, ולהיפך).

ללא אסוציאציות פולימורפיות, יהיה לנו משהו כזה:

class Post belongs_to :person belongs to :groupend
class Person has_many :postsend
class Group has_many :postsend

בדרך כלל, כדי לברר מי הבעלים של פרופיל מסוים, אנו מסתכלים בעמודה שהיא foreign_key. A foreign_keyהוא מזהה המשמש למציאת האובייקט הקשור בטבלת המודל הקשור.

עם זאת, בטבלת ההודעות שלנו יהיו שני מפתחות זרים מתחרים: group_idו- person_id. זה יהיה בעייתי.

כשמנסים למצוא את הבעלים של ההודעה, נצטרך לשים לב לבדוק בשתי העמודות כדי למצוא את מפתח הזר הנכון, במקום להסתמך על אחד. מה קורה אם נקלע למצב שלשני העמודות יש ערך?

אגודה פולימורפית מטפלת בבעיה זו על ידי עיבוי פונקציונליות זו לאסוציאציה אחת. אנו יכולים לייצג את השיעורים שלנו כך:

class Post belongs_to :postable, polymorphic: trueend
class Person has_many :posts, as :postableend
class Group has_many :posts, as :postableend

אמנת Rails למתן שם לאסוציאציה פולימורפית משתמשת ב- "able "עם שם הכיתה ( :postableעבור Postהכיתה). זה מבהיר במערכות היחסים שלך איזה מעמד הוא פולימורפי. אבל אתה יכול להשתמש בכל שם עבור הקשר הפולימורפי שאתה אוהב.

כדי לספר למאגר שלנו אנו משתמשים באסוציאציה פולימורפית, אנו משתמשים בעמודות "סוג" ו"איד "מיוחדות עבור המעמד הפולימורפי.

postable_typeרשום הטור איזה דגם הפוסט שייך, בעוד postable_idהטור עוקב id של האובייקט בעלות:

haley = Person.first=> returns Person object with name: "Haley"
article = haley.posts.firstarticle.postable_type=> "Person"
article.postable_id=> 1 # The object that owns this has an id of 1 (in this case a Person)
new_post = haley.posts.new()# Automatically fills in postable_type and postable_id using haley object

A polymorphic association is just a combination of two or more belongs_to associations. Because of this, you can act the same way you would when using two models that have a belongs_to association.

Note: polymorphic associations work with both has_one and has_many associations.

haley.posts# returns ActiveRecord array of posts
haley.posts.first.content=> "The content from my first post was a string..."

One difference is going “backwards” from a post to access its owner, since its owner could come from one of several classes.

To do that quickly, you need to add a foreign key column and a type column to the polymorphic class. You can find the owner of a post using postable:

new_post.postable=> returns Person object
new_post.postable.name=> "Haley"

Additionally, Rails implements some security within polymorphic relationships. Only classes that are part of the relationship can be included as a postable_type:

new_post.update(postable_type: "FakeClass")=> NameError: uninitialized constant FakeClass

Warning

Polymorphic associations come with one huge red flag: compromised data integrity.

In a normal belongs_to relationship, we use foreign keys for reference in an association.

They have more power than just forming a link, though. Foreign keys also prevent referential errors by requiring that the object referenced in the foreign table does, in fact, exist.

If someone tries to create an object with a foreign key that references a null object, they will get an error.

Unfortunately, polymorphic classes can’t have foreign keys for the reasons we discussed. We use the type and id columns in place of a foreign key. This means we lose the protection that foreign keys offer.

Rails and ActiveRecord help us out on the surface, but anyone with direct access to the database can create or update objects that reference null objects.

For example, check out this SQL command where a post is created even though the group it is associated with doesn’t exist.

Group.find(1000)=> ActiveRecord::RecordNotFound: Couldn't find Group with 'id'=1000
# SQLINSERT INTO POSTS (postable_type, postable_id) VALUES ('Group', 1000)=> # returns success even though the associated Group doesn't exist

Thankfully, proper application setup can prevent this from being possible. Because this is a serious issue, you should only use polymorphic associations when your database is contained. If other applications or databases need to access it, you should consider other methods.

Polymorphic association PROS:

  • Easy to scale in amount of data: information is distributed across several database tables to minimize table bloat
  • Easy to scale number of models: more models can be easily associated with the polymorphic class
  • DRY: creates one class that can be used by many other classes

Polymorphic association CONS

  • More tables can make querying more difficult and expensive as the data grows. (Finding all posts that were created in a certain time frame would need to scan all associated tables)
  • Cannot have foreign key. The id column can reference any of the associated model tables, which can slow down querying. It must work in conjunction with the type column.
  • If your tables are very large, a lot of space is used to store the string values for postable_type
  • Your data integrity is compromised.

How to know which method to use

STI and polymorphic associations have some overlap when it comes to use cases. While not the only solutions to a “tree-like” model relationship, they both have some obvious advantages.

Both the Vehicle and Postable examples could have been implemented using either method. However, there were a few reasons that made it clear which method was best in each situation.

Here are four factors to consider when deciding whether either of these methods fits your needs.

  1. Database structure. STI uses only one table for all classes in the relationship, while polymorphic associations use a table per class. Each method has its own advantages and disadvantages as the application grows.
  2. Shared data or state. STI is a great option if your models have many shared attributes. Otherwise a polymorphic association is probably the better choice.
  3. Future concerns. Consider how your application might change and grow. If you’re considering STI but think you’ll add models or model fields that deviate from the shared structure, you might want to rethink your plan. If you think your structure is likely to remain the same, STI will generally be faster for querying.
  4. Data integrity. If data is not going to be contained (one application using your database), polymorphic association is probably a bad choice because your data will be compromised.

Final Thoughts

Neither STI nor polymorphic associations are perfect. They both have pros and cons that often make one or the other more fit for associations with many models.

I wrote this article to teach myself these concepts just as much as to teach them to anyone else. If there is anything incorrect or any points you think should be mentioned, please help me and everyone else out by sharing in the comments!

If you learned something or found this helpful, please click on the ? button to show your support!