Problems While Making a Generic Model in Ruby on Rails 3

Problems while making a generic model in Ruby on Rails 3

Why not simply create an ActiveRecord::Base subclass at runtime and avoid all the hassle?

t = 'some_table'
c = Class.new(ActiveRecord::Base) { self.table_name = t }

then c refers to an AR class for some_table and you can do the usual things:

o = c.find(1)
# 'o' is now a wrapper for the row of some_table where 'id = 1'

cols = c.columns.map(&:name)
# 'cols' is now an array of some_table's column names

This is Ruby where classes are objects too.

If you need to connect to another database then you can to put the establish_connection call in the block along with the self.table_name:

t = 'some_table'
d = 'some_other_database'
c = Class.new(ActiveRecord::Base) do
establish_connection(:adapter => 'mysql2', :database => d, ...)
self.table_name = t
end

How to localize a generic error messages partial in Rails 3.2.3?

OK, I found the answer myself this time, thanks to this thread.

I simply changed my _error_messages.html.rb partial to:

<% if object.errors.any? %>
<div id="error_explanation">
<h3><%= t('errors.template.header', :model => object.class.model_name.human, :count => object.errors.count) %></h3>
<p><%= t('errors.template.body') %></p>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

Now it works and I am happy :-)

Creating and storing generic methods in ruby on rails

Answers to your question will necessarily be subjective because there are always be many answers to "where should I put functionality?", according to preference, principle, habit, customs, etc. I'll list a few and describe them, maybe add some of my personal opinions, but you'll ultimately have to choose and accept the consequences.

Note: I'll commonly refer to the common degenerate case of "losing namespacing scope" or "as bad as having global methods".

Monkeypatch/Extend String

Convenient and very "OO-message-passing" style at the cost of globally affecting all String in your application. That cost can be large because doing so breaks an implicit boundary between Ruby core and your application and it also scatters a component of "your application" in an external place. The functionality will have global scope and at worst will unintentionally interact with other things it shouldn't.

Worthy mention: Ruby has a Refinements feature that allows you to do do "scoped monkeypatching".

Worthy mention 2: Ruby also lets you includes modules into existing classes, like String.class_eval { include MyCustomization } which is slightly better because it's easier to tell a customization has been made and where it was introduced: "foo".method(:custom_method).owner will reveal it. Normal Monkeypatching will make it as if the method was defined on String itself.

Utils Module

Commonly done in all programming languages, a Util module is simply a single namespace where class methods/static methods are dumped. This is always an option to avoid the global pollution, but if Util ends up getting used everywhere anyways and it gets filled to the brim with unrelated methods, then the value of namespacing is lost. Having a method in a Util module tends to signify not enough thought was put into organizing code, since without maintenance, at it's worst, it's not much better than having global methods.

Private Method

Suppose you only need it in one class -- then it's easy to just put it into one private method. What if you need it in many classes? Should you make it a private method in a base class? If the functionality is inherent to the class, something associated with the class's identity, then Yes. Used correctly, the fact that this message exists is made invisible to components outside of that class.

However, this has the same downfall as the Rails Helper module when used incorrectly. If the next added feature requires that functionality, you'll be tempted to add the new feature to the class in order to have access to it. In this way the class's scope grows over time, eventually becoming near-global in your application.

Helper Module

Many Rails devs would suggest to put almost all of these utility methods inside rails Helper modules. Helper modules are kind of in between Utils Module and Private Method options. Helpers are included and have access to private members like Private Methods, and they suggest independence like Utils Modules (but do not guarantee it). Because of these properties, they tend to end up appearing everywhere, losing namespacing, and they end up accessing each other's private members, losing independence. This means it's more powerful, but can easily become much worse than either free-standing class/static methods or private methods.

Create a Class

If all the cases above degenerate into a "global scope", what if we forcibly create a new, smaller scope by way of a new class? The new class's purpose will be only to take data in and transform it on request on the way out. This is the common wisdom of "creating many, small classes", as small classes will have smaller scopes and will be easier to handle.

Unfortunately, taking this strategy too far will result in having too many tiny components, each of which do almost nothing useful by themselves. You avoid the ball of mud, but you end up with a chunky soup where every tiny thing is connected to every other tiny thing. It's just as complicated as having global methods all interconnected with each other, and you're not much better off.

Meta-Option: Refactor

Given the options above all have the same degenerate case, you may think there's no hope and everything will always eventually become horribly global -- Not True! It's important to understand they all degenerate in different ways.

Perhaps functionality 1, 2, 3, 4... 20 as Util methods are a complete mess, but they work cohesively as functionality A.1 ~ A.20 within the single class A. Perhaps class B is a complete mess and works better broken apart into one Util method and two private methods in class C.

Your lofty goal as an engineer will be to organize your application in a configuration that avoids all these degenerate cases for every bit of functionality in the system, making the system as a whole only as complex as necessary.

My advice

I don't have full context of your domain, and you probably won't be able to communicate that easily in a SO question anyways, so I can't be certain what'll work best for you.

However, I'll point out that it's generally easier to combine things than it is to break them apart. I generally advise starting with class/static methods. Put it in Util and move it to a better namespace later (Printer?). Perhaps in the future you'll discover many of these individual methods frequently operate on the same inputs, passing the same data back and forth between them -- this may be a good candidate for a class. This is often easier than starting off with a class or inheriting other class and trying to break functionality apart, later.

Generic model that has many relationship depending on value of enum

Your idea of using a enum won't work here since assocations are class level and the value of the enum is only known on the instance level.

If you really wanted to use an enum you could hack something together with an instance method but it won't really behave like an assocation when it comes to stuff like eager loading:

class Blog < ApplicationRecord
# ...
def posts
send("#{platform}_posts")
end
end

What you can do is use Single Table Inheritance to setup classes that share a table yet have different behavior.

First add a type column to the table:

class AddDetailsToBlogs < ActiveRecord::Migration[6.0]
def change
change_table :blogs do |t|
t.remove :platform
t.string :type, index: true, null: false
end
end
end

If you have existing data you should go through it and set the type column based on the value of platform before you drop platform and make type non-nullable.

Then setup the subclasses:

class Blog < ApplicationRecord
# shared behavior
end
class WordPressBlog < Blog 
has_many :posts,
class_name: 'WordPressPost',
foreign_key: :blog_id,
inverse_of: :blog
end
class DrupalBlog < Blog 
has_many :posts,
class_name: 'DrupalPost',
foreign_key: :blog_id,
inverse_of: :blog
end

The main advantage of STI is that it lets you query as a single table and thus treat it as a homogenous collection, the drawbacks are that you are potentially wasting database space with columns containing largely nulls and it can become quite unweildy if the types differ to much from each other.

Generics Example Question

The first thing to realize about the scaffolding code is that it can be abreviated, as such:

def index
@users = User.all
end

unless you intend to deliver the view in another format, like json, html, pdf, the respond_to block is unnecessary. If you still feel the need to dry up this method, you could do something like

# app/controllers/concerns/autoload_records.rb

module AutoloadRecords
included do
before_action :load_records, only: :index
before_action :load_record, only: [:create, :show, :edit, :update, :destroy]
end

private
def load_records
@records = model_class.all
end

def load_record
@record = model_class.find(params[:id])
end

def model_class
klass = self.class.to_s[/\A(\w+)sController\Z/,1] #=> get the name of the class from the controller Constant
Object.const_get(klass)
end
end

and write your controller like

class UsersController < ApplicationController
include AutoloadRecords

def index
@records # => #<ActiveRecord::Relation[...]>
end

def show
@record # => #<User ...>
end

def non_rest_action
@record # => nil
@records # => nil
end
end

Rails 3 - Concerns with Modules or Classes

If your model is Alert, you definitely don't want module Alert (#3). #1 and #2 are basically the same, but more often you see the #2 style.

Let me explain a little further.

The module X::Y style will only work if X has already been defined. It's saying "create this module Y under X and I don't care if X is a class or module, just do it.

For #3, since Alert is already defined as a class, you'll get this error: TypeError: Alert is not a module.

Let me know if you need more clarification.

Ruby on Rails: Assigning attribute values to generic model

assign_attributes expects a hash of attributes to be passed to it. You are passing it a string. Would it be problematic to simply say b = {attr_name.to_sym => 9}?

Generic flags for a model in RoR

In my opinion Promotion should be a separate model with a many to many relationship with User. When you have a promotion you would create a Promotion instance and when a person uses that promotion you add that person to promotion.users relationship.

This is much better than your idea because you can now query those relationship. Want a list of all users that used the first quarter promotion? No problem. You can do that with your solution, but you have to resort to some hackiness (is that a word?) to do it, and you'd have to parse the generic flag string for EVERY user on EVERY query. Not ideal to say the least.



Related Topics



Leave a reply



Submit