I'm a bit confused about STI in rails.
My situation:
I have a Contact model that has description and data string fields, to store some contact like phone, fax, email, etc.
Now when I have some specific contact type like phone number of email address I want to walidate the data format in different way and I want to make some different formating on output.
I decided to use STI as all the models have the same data with just different behaviour. And I have some questions regarding forms and CRUD operations as I don't want to go against Rails conventions.
How do I make a dropdown list in form with model type? Should I hardcode it or is there some more automated way?
How do I create a record? Should I use switch statement and according to received type create new model of according instance?
How should I update it if I'm going to change the model type? Cast the object to new class? Or create a new object and destroy the previous one?
I'll be very thankfull for your help!
Yes, should do a hardcore as there no default store for your STI models.
Generally, yes. But With Rails you could just use camelize.constantize to get class from string. Another way is just use parent model, and set type field manually. As with STI all records are in the same table and then all are of the parent class.
If you wish to update, just update type field. Then you could re-query to force Rails to get new object of different type.
You could create a model like this :
Type < ActiveRecord::Base
has_many :contacts
end
You could use this command rails g model Type name:string, add a type_id column in your contact and migrate the database.
end change your contact's model like this :
Contact < ActiveRecord::Base
belongs_to :type
end
Now, in your form, you could use this :
select("type", "type_id", Type.all.collect {|t| [ t.name, t.id ] }, { :include_blank => true })
It should resolve your problem.
Now you can do something like this :
#emails = Type.find_by_name('email').contacts
Or use scopes.
Related
I have two types of users (regular user, super user). What is the proper way to extend one base user class with additional tables?
I was thinking something like this but I am not sure am I going to right direction:
class User < ActiveRecord::Base
end
class SuperUser < User
end
class RegularUser < User
end
Is this the proper way to do it in Rails? Thanks :)
It is 100% correct approach, however you need to remember, that all your models will be stored in one table in database. This approach is called STI (Single table inheritance) and requires only one additional field type in you model.
If you want to have different types of users I would go with user roles versus different user tables etc.
A very good gem for that is CanCan and the documentation is excellent:
https://github.com/ryanb/cancan/wiki/Role-Based-Authorization
You will also have nice helpers as .can? or .cannot? and more.
Yes, and you should also use single table inheritance. What this means is you should add a column called 'type' to your user model. Rails recognizes the column 'type' and treats it special. Essentially, all entries in your type model will reference another model. In that model, you can define rules for each type. It would also be a good idea to validate your user model so that only the two types you want can be entered. This should work:
class User < ActiveRecord::Base
validates :type, :inclusion => {:in => ['SuperUser', 'RegularUser']}
end
So in my rails app, I'm creating a table of users, and in that table I have columns such as "email, password, and image". I want to add a concrete list of nationalities that a user can be associated with.
So, I created a user model:
rails g model User email password image:string
Now I want an array of nationalities... so instead of creating an array, I created a separate model for nationalities and want to associate it to my users. Assuming one can have multiple nationalities, I set it up like this:
rails g model Nationality name:string
So in user.rb I wrote:
has_many :nationalities
and in nationality.rb I wrote:
belongs_to :user
When a user signs up, I will have a list for them to choose from: American, Canadian, French, etc...
My gut tells me this might be an antipattern since a user won't ever be adding new nationalities to the list... only selecting, but an admin can always add more nationalities.
I'm not getting any errors, but it isn't working in my favor either.
Did I set it up correctly, or should I just use an array and store it in the user's table under a column called "nationalities"?
edit: Oops, I didn't address the part of having multiple nationalities. You may want to consider using acts_as_taggable_on
https://github.com/mbleigh/acts-as-taggable-on
It nicely handles the many-to-many behavior of tagging with some built in conveniences.
I think it's fine to use some kind of global array (in a namespace) for Nationalities for now, because as you note correctly, the kind of Nationalities will not change.
However, I think things will be a little more complicated than a straight-up Array...You say you will give choices such as American, Canadian, French...and that's fine, but what if the context of a view calls for using U.S., Canada, France instead? Then your Array becomes a Hash...I doubt it'll get complicated enough to require a full fledged Rails model. A simple Hash should cover most of those use cases.
I've found it helpful in such situations to create a separate Class or Module. For example, with Gender, it's most definitely not worth creating a separate DB table...and yet, even if all I store in a user's gender column is "M", "F", "O" (for other, or maybe not specified), I'll create a Gender module that can handle that domain logic:
class Gender
# better to make this private so that
# only the factory constructor is exposed
def initialize(g)
#g = g
end
def to_s # and equivalent methods for serializing Gender into a M/F for database
return #g.to_s
end
def pronoun
case #g
when :M
'he'
when :F
'she'
else
'they'
end
end
def Gender(str)
case str
when /^(?:m|male|man|boy)$/i
Gender.new(:M)
when /^(?:f|female|woman|girl)$/i
Gender.new(:F)
else
Gender.new(:O)
end
end
This is really more a database design question than a Rails question, but I would just have the Nationality table serve as the source for a dropdown, but the User table would simply have a nationality column. I don't see the need for the extra join. Of course, the values in the nationality column would come from user selections from that dropdown populated by the Nationality table.
Still, your needs may vary. Stick with the join if multiple nationalities are a requirement. As Knuth said, "Premature optimization is the root of all evil."
Are you using Rails 4? Is Postgresql your DB? If so this combination allows you to store arrays on your user model.
Basically you would modify your migration to look like this:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name # or whatever attributes you've already defined
t.string :nationalities, array: true, default: []
t.timestamps
end
end
end
Your ActiveRecord model would contain this:
class User < ActiveRecord::Base
attr_accessible :nationalities
end
This would allow you to create the nationalities list like so:
user = User.create(name: 'Bob', nationalities: ['English', 'Australian', 'French'])
And obviously extract them like:
user.nationalities
=> ['English', 'Australian', 'French']
Still, the decision about having an association or a column on the user goes beyond the ease of creating the user with a nationality in a form. The real question is "what role will the nationality play in the app"? Meaning, does the nationality have additional data that would warrant it being it's own model? Or, can it all just sit in the user table?
If the nationality has as much functionality as a user name, than it can stay on the user. However, if nationality can have other attributes on it such as 'date_aquired' than it may have to be it's own model.
Please help a newbie to choose the best way to implement inheritance in RoR3. I have:
-Person (address fields, birthdate, etc.)
-Player, inherits from Person (position, shoe_size, etc.)
-Goalkeeper, inherits from Player (other specific fields related to this role)
I think that Single Table Inheritance is a bad solution, because there will be a lot of null fields in the table created. What is the best way to do this? Use polymorphic associations (with has_one?)? Use belongs_to/has_one (but then how to show in the Player views the fields of Person too?)? Don't implement inheritance? Other solutions?
While I think STI is probably the approach I would use for this, one other possibility, if you want to avoid a lot of NULL attributes, is to add a column other_attributes to your Person model that will store a Hash of attributes. To do this, add a text column to the people table:
def self.up
add_column :people, :other_attributes, :text
end
Then make sure the attribute is serialized in the model. And you may want to write a wrapper to make sure it's initialized as an empty Hash when you use it:
class Person < ActiveRecord::Base
serialize :other_attributes
...
def other_attributes
write_attribute(:other_attributes, {}) unless read_attribute(:other_attributes)
read_attribute(:other_attributes)
end
end
Then you can use the attribute as follows:
p = Person.new(...)
p.other_attributes #=> {}
pl = Player.new(...)
pl.other_attributes["position"] = "forward"
pl.other_attributes #=> {"position" => "forward"}
One caveat with this approach is that you should use strings as keys when retrieving data from other_attributes, as the keys will always be strings when the Hash is retrieved from the database.
I suggest STI. An alternative solution is to use a document store like mongodb, or use the activerecord store http://api.rubyonrails.org/classes/ActiveRecord/Store.html. If you have a postgress database look at his HStore column http://rubygems.org/gems/activerecord-postgres-hstore.
Another option is PostgreSQL table inheritance. http://www.postgresql.org/docs/8.1/static/ddl-inherit.html
models:
#StatusMessage model
class StatusMessage < ActiveRecord::Base
belongs_to :users
default_scope :order => "created_at DESC"
end
#User Model
class User < ActiveRecord::Base
has_many :status_messages
end
In controller I want to join these two tables and get fields from both table. for example I want email field from User and status field from StatusMessage. When I use :
#status = User.joins(:status_messages)
Or
#status = User.includes(:status_messages)
It gives me only the user table data.
How can I implement this requirement?
You need to use includes here. It preloads data so you won't have another SQL query when you do #user.status_messages.
And yes you can't really see the effect: you need to check your logs.
First of all, I don't think it is possible (and reasonable) what you want to do. The reason for that is that the relation between User and StatusMessage is 1:n, that means that each user could have 0 to n status messages. How should these multitudes of attributes be included in your user model?
I think that the method joints in class ActiceRecord has a different meaning, it is part of the query interface. See the question LEFT OUTER joins in Rails 3
There are similar questions on the net, here is what I have found that matches most:
Ruby on Rails: How to join two tables: Includes (translated for your example) in the user a primary_status_message, which is then materialized in the query for the user. But it is held in one attribute, and to access the attributes of the status_message, you have to do something like that: #user.primary_status_message.status
When you use #status = User.includes(:status_messages) then rails eagerley loads the data of all the tables.
My point is when you use this User.includes(:status_messages) it will loads the data of status_messages also but shows only users table data then if you want first user status_messages then you have to #user.first.status_messages
Hey I am stuck with my orientation in rails.
I got a User model, a Course Model and a CourseEnrollment Model.
When I want to add a link in my Course Index View like
link_to 'join' CourseEnrollment.create(:course_id => course.id, :user_id => current_user)
Does this create method belong to my Model? I am confused because in my User Model I defined a method that uses role_assignments.create(.....). What is the difference between these 2 create methods? I cant use course_enrollments.create by the way. Thx for your time
I'm a bit confused as to what you're asking, but I'll try my best.
(First of all, in your example, current_user should probably be current_user.id.)
When you call CourseEnrollment.create, you are simply creating a new CourseEntrollment model with the specified attributes.
Assuming that your User model has_many :role_assignments:
When you call #role_assignments.create from within your User model, Rails automatically creates the association for you (e.g. sets the user_id to the id of the user). This doesn't have to be done within the model itself, though:
current_user.role_assignments.create(...) # automatically sets the association
Assuming that your User model also has_many :course_enrollments, the following will create a CourseEnrollment model and automatically associate it with the current user:
current_user.course_enrollments.create(...)