📜 ⬆️ ⬇️

Polymorphic bonds

The other day, an article about polymorphic links appeared in the Ruby on Rails blog, in which the author wrote all sorts of different things, but forgot to mention how to use them and why they are needed (then, of course, corrected, but still wrote quite superficially).
At first, I was even scared that this article of mine in some incomprehensible way escaped from the “drafts” and got into the general tape. Then I figured it out, collected my thoughts, and decided to finish my own.

What are polymorphic links and what are they for? In one of his screencasts, Ryan Bates has already talked about this, and in no case do I want to say the same thing. The situation was as follows:
we have models Articles , Photos and Events . And there is a model Comments . And I really want to keep all comments (comments of articles, photos and events) in one table.
There are a lot of articles on this problem on the Internet, but there are also cases of “vice versa”. You don’t need to go far, let's try to develop the functionality of Habrahabr posts!

Here we can write articles, while the articles themselves can be of different types: Topic, Link, Question, Podcast, Translation and Vacancy. But you must admit, it would be rather foolish to create six separate tables containing almost identical fields: heading, date of creation / change, various seo information, tags, status (published / not) and necessarily something else.

I wanted to paint everything “for the smallest,” but around the middle of the article I realized that somehow it turns out a lot (and they use polymorphic connections, mostly not newbies). And yes, I completely forgot: let's cut down a bit our functionality and we will design the links at the rate of 3 models: Topic, Link and Podcast (and it really hurts a lot of code, but everything is done in the same way.
')
So let's go!
Create 4 models: Post, Topic, Link and Podcast. The incomprehensible Post model will be the “parent” one for the others and, in fact, it will contain all unnecessary common fields.
bash-3.2$ script/generate model post title:string published:boolean content_id:integer content_type:string
bash-3.2$ script/generate model topic body:text
bash-3.2$ script/generate model podcast link:string description:text
bash-3.2$ script/generate model link link:string description:text
As you can see, links and podcasts have the same fields, let's make another polymorphic link :)

When creating the Post migration, we specified common fields for all other tables (in this case, the title (title), the status of the article (published) and the date of creation / modification (they were added automatically)). In addition, there are 2 fields in which the element ID (content_id) and the model that owns this element will be stored (content_type; we will deal with this a little later).
We will not touch the migration of the Post model anymore, but in all other migrations we will delete this line:
t.timestamps
After all, the fields created_at and updated_at (which are generated by the helper timestamps) are now one for all - in the posts table.

We do rake db: migrate and ... and the final touch remains: add connections to the models.
# app/models/post.rb <br/> class Post < ActiveRecord::Base <br/> belongs_to :content, :polymorphic => true , :dependent => :destroy<br/> end <br/> <br/> # app/models/topic.rb <br/> class Topic < ActiveRecord::Base <br/> has_one :post, :as => :content, :dependent => :destroy<br/> end <br/> <br/> # app/models/link.rb <br/> class Link < ActiveRecord::Base <br/> has_one :post, :as => :content, :dependent => :destroy<br/> end <br/> <br/> # app/models/podcast.rb <br/> class Podcast < ActiveRecord::Base <br/> has_one :post, :as => :content, :dependent => :destroy<br/> end <br/>
Instead of writing “has_many: links (: topics,: podcasts)” in the Post model, we say that Post is tightly tied by family ties with a polymorphic link with some: content, and now any model in which we write
:has_one :post, :as => content
will become a subsidiary of our Post'a. What we, in fact, have done above.
Now we are fully ready to go to the console and rejoice :)
bash-3.2$ script/console
>> t = Topic.new(:body => "Just one more test topic body here")
>> t.save
>> p = Post.new(:title => "Some test title", :published => true, :content => t)
>> p.save
They created a new topic (indicating only the body), saved, created a new post (indicating the title, status and content itself (you could write and: content_id => t.id,: content_type => t.class (as if implying also .to_s)).

Without a doubt, so that the content_type field is immediately filled with a value, we can write this:
>> Post.topics.new
=> #<Post id: nil, title: nil, published: nil, content_id: nil, content_type: "Topic", created_at: nil, updated_at: nil>

Let's try to view all the topics:
>> Post.find_all_by_content_type("Topic")
I agree, uncomfortable; let's add some named_scope to the Post model:
named_scope :topics, :conditions => { :content_type => "Topic" }<br/>named_scope :links, :conditions => { :content_type => "Link" }<br/>named_scope :podcasts, :conditions => { :content_type => "Podcast" } <br/>
We go again to the console, do reload! and look around:
>> Post.topics
>> Post.links
>> Post.podcasts
Now you need to understand how to access all the properties of our posts.
>> p.body
NoMethodError: undefined method `body' for #<Post:0x2653e00>
>> t.title
NoMethodError: undefined method `title' for #<Topic id: 8, body: "Just one more test topic body here">
It turns out that not so :) Let's try something like this:
>> p.content.body
=> "Just one more test topic body here"
>> t.post.title
=> "Some test title"
Played and that's enough, it's time to make controllers (and there and close to the presentation). We leave from the rail console, we are waiting for the usual :)
bash-3.2$ script/generate controller posts index
bash-3.2$ script/generate controller posts/topics index show
bash-3.2$ script/generate controller posts/podcasts index show
bash-3.2$ script/generate controller posts/links index show
bash-3.2$ script/generate controller home index
Next we go to config / routes.rb and bring it to this view:
ActionController::Routing ::Routes.draw do |map|<br/> map.root :controller => 'home' <br/> <br/> map.namespace(:posts) do |post|<br/> post.resources :topics, :links, :podcasts<br/> end <br/> map.resources :posts<br/> <br/> map.connect ':controller/:action/:id' <br/> map.connect ':controller/:action/:id.:format' <br/> end <br/>
And now let's start the server and see what we have: bash-3.2$ script/server
And we got this:
/posts ( )
/posts/topics ( — –)
/posts/links ( — –)
/posts/podcasts ( , ;)

Of course, the whole REST is available, you can even have no doubt about it;)

Now fill the code with the controllers:
# app/controllers/posts_controller.rb <br/> class PostsController < ApplicationController<br/> def index <br/> @posts = Post.find(:all)<br/> end <br/> end <br/> <br/> # app/controllers/posts/topics_controller.rb <br/> class Posts ::TopicsController < ApplicationController<br/> def index <br/> @posts = Post.topics.find(:all)<br/> end <br/> <br/> def show <br/> @post = Post.topics.find(params[:id])<br/> end <br/> end <br/> <br/> # app/controllers/posts/links_controller.rb <br/> class Posts ::LinksController < ApplicationController<br/> def index <br/> @posts = Post.links.find(:all)<br/> end <br/> <br/> def show <br/> @post = Post.links.find(params[:id])<br/> end <br/> end <br/> <br/> # app/controllers/posts/podcasts_controller.rb <br/> class Posts ::PodcastsController < ApplicationController<br/> def index <br/> @posts = Post.podcasts.find(:all)<br/> end <br/> <br/> def show <br/> @post = Post.podcasts.find(params[:id])<br/> end <br/> end <br/>
In posts_controller, for the time being, we only fill in the index, show is not needed there. In the rest we fill in both the index (there, as you can see, only the “necessary” posts will be displayed), and show (and the article / link / podcast will be displayed here). I think, here you can do without explanations, we have already written all this code in the console.

Immediately proceed to the views, and the first - posts # index:
<!-- app/views/posts/index.html.erb --><br/><% @posts.each do |post| %><br/> <%= link_to post.content. class . to_s .pluralize, "/posts/#{post.content.class.to_s.downcase.pluralize}" %> &rarr;<br/> <%= link_to post.title, "/posts/#{post.content.class.to_s.downcase.pluralize}/#{post.id}" %><br/><br/><% end %> <br/>
At first, I wrote this way, because to count down on tons of ifs is even worse (IMHO). Then I felt ashamed that people would see such horror on Habré, and decided to make this horror a little less awful. So, open app / helpers / posts_helper.rb and write something like
module PostsHelper<br/> def posts_smth_path (post)<br/> case post.content. class . to_s .downcase<br/> when "topic" : posts_topic_path(post)<br/> when "link" : posts_link_path(post)<br/> when "podcast" : posts_podcast_path(post)<br/> end <br/> end <br/> <br/> def posts_smths_path (post)<br/> case post.content. class . to_s .downcase<br/> when "topic" : posts_topics_path<br/> when "link" : posts_links_path<br/> when "podcast" : posts_podcasts_path<br/> end <br/> end <br/> end <br/>
Now we have 2 methods: posts_smth_path and posts_smths_path, which are a special case of posts_topic_path and posts_topics_path (instead of topic / topics, of course, there may also be link / links and podcast / podcasts). Having done the work on the bugs, we look at what we did:
<!-- app/views/posts/index.html.erb --><br/><% @posts.each do |post| %><br/> <%= link_to post.content. class . to_s .pluralize, posts_smths_path(post) %> &rarr;<br/> <%= link_to post.title, posts_smth_path(post) %><br/><br/><% end %> <br/>
I think that is enough for a draft. Now the rest of the submission:
<!-- app/views/posts/topics/index.html.erb --><br/><% @posts.each do |post| %><br/> <%= link_to post.title, posts_topic_path(post) %><br/><br/><% end %><br/>< p ><br/> <%= link_to "Add new Topic" , new_posts_topic_path %><br/></ p > <br/>
This is the index method, and with the exception of the posts_topic_path and new_posts_topic_path methods it is the same everywhere, it makes no sense to build a ton of code here. The other two will be posts_link_path / new_posts_link_path and posts_podcast_path / new_posts_podcast_path respectively.
<h1><%= @post.title %></h1><br/><%= @post.content.body %> <br/>
And this is a show, and in this example it is generally the same everywhere :)

And now - perhaps the most interesting: adding new records. As you have already noticed, in the previous listing there is a line
<%= link_to "Add new Topic" , new_posts_topic_path %>
The link_to helper will generate a link that, when clicked, will go to page / posts / topics / new, so it’s just vital for us to create the file app / views / posts / topics / new.html.erb and write something like this to it:
<!-- app/views/posts/topics/ new .html.erb --><br/><% form_for [:posts, @topic] do |form| %><br/> <% form .fields_for @post do | p | %> <br/> < p ><br/> <%= p .label :title %><br/><br/> <%= p .text_field :title %> <br/> </ p ><br/> < p ><br/> <%= p .check_box :published %><br/> <%= p .label :published %><br/> </ p ><br/> <% end %><br/> <br/> < p ><br/> <%= form .label :body %><br/><br/> <%= form .text_area :body %> <br/> </ p ><br/> <br/> < p ><%= form .submit "Create" %></ p ><br/><% end %> <br/>
At once I will make a reservation, so far it will only be about topics, in the rest of the controllers / views there will be a similar code.

To make everything fall into place, I will give the code for the new method of the topics controller:
def new <br/> @topic = Topic. new <br/> @post = Post.topics. new <br/> end <br/>
And, for complete clarity, I will repeat the code that we wrote in the routes.rb file:
map.namespace(:posts) do |post|<br/> post.resources :topics, :links, :podcasts<br/> end <br/>
Once upon a time we defined a namespace, and now when creating forms for topics instead of form_for topic do ... we will specify our namespace, that is, write form_for [: posts, topic ] do ... (similarly for links and podcasts).
At the very end of the form, we put the body field and the submit button of the form, and before that we use the fields_for helper, which is similar in behavior to the form_for helper, unless it creates form tags. Thus, we get 2 forms, as if, one being nested in another.

Fill out the form, click the Create button and go to the create method of the topics controller. Let's write something working in it, and adding articles is ready!
def create <br/> @topic = Topic. new (:body => params[:topic][:body])<br/> if @topic.save<br/> @post = Post. new ({ :content => @topic }.merge params[:topic][:post])<br/> if @post.save<br/> redirect_to root_url<br/> else <br/> render :new<br/> end <br/> else <br/> render :new<br/> end <br/> end <br/>
I sincerely apologize for such an abundance of code in the method, I am sure that this code can (and should!) Be put into the model, but I do not know how. I hope some of the more experienced comrades will correct me.

On this, I think, everything. Updating elements is done in a similar way to creation, with this should not be difficult. I apologize for errors, typos, tediousness and for the size of the article: I did not want to!

Treat with all severity, this is not my first article on Habré!

UPD:
Useful materials on the topic:
STI - one table and many models.
Creating multi-model forms

Source: https://habr.com/ru/post/79431/


All Articles