Has and belong to many Rails 4 - ruby-on-rails

I want to create a many to many relationship between two models, and I'd like to know step by step what to do. I'd like to have a explanation on HOW to do the migrations and HOW to ideally create the models. The way i'm trying to do right now is:
I create two models in the Ruby command line:
rails g model Location name:string
rails g model Datetime date:datetime
Now I have to open the recently created models and add:
//models/location.rb
class Location < ActiveRecord::Base
has_and_belong_to_many :datetimes
end
//models/datetime.rb
class Datetime< ActiveRecord::Base
has_and_belong_to_many :locations
end
Now apparently I have to do a migration, but I don't understand what it is, and the I guess some sources for oldest versions really got me confused. Can someone please explain in details?
Obs: There are some similar questions but they do not answer my question because they don't explain in depth what to do.

As suggested by ruby tutorial, we generate a new migration:
rails g migration CreateDatetimesAndLocations
Inside this migration i have:
class CreateDatetimesAndLocations < ActiveRecord::Migration
def change
create_table :locations_datetimes, id:false do |t|
t.belongs_to :datetime, index: true
t.belongs_to :location, index: true
end
end
end
This is done exactly like the ruby tutorial. Now I have this controller, that i'm testing on and it goes like that:
class WeatherController < ApplicationController
def data
#location = Location.new
#location.name = "test"
#datetime = Datetime.new
#datetime.date = DateTime.new(2001,2,3)
#location.datetimes << #datetime // **PROBLEM ON THIS LINE**
#location.save
#location = Location.new
#location.name = "teste2"
#location.locations << #location
#locations = Location.all //(show locations in view)
end
end
The problem I was having was because the locations_datetimes have to be datetimes_locations (in alphabetic order apparently).

Related

Make a generator of models inheriting from a pre-built model

I'm writing a little Ruby on Rails CMS, and I want to make a generator that would create models inheriting from the pre-built CMS data model. For example, I have a Entry model in my CMS, and I want to create a Post model that would be child of this model. This is the source code:
db/migrations/*create_mycms_entries.rb:
class CreateMyCmsEntries < ActiveRecord::Migration[5.0]
def change
create_table :my_cms_entries do |t|
t.string :type, index: true
t.string :title
t.string :slug, index: true
t.json :payload
t.integer :user_id, index: true
t.string :author_name
t.datetime :published_at
t.timestamps null: false
end
end
end
entry.rb:
module MyCms
class Entry < ActiveRecord::Base
scope :published, -> { where('published_at <= ?', Time.zone.now) }
def self.content_attr(attr_name, attr_type = :string)
content_attributes[attr_name] = attr_type
define_method(attr_name) do
self.payload ||= {}
self.payload[attr_name.to_s]
end
define_method('#{attr_name}='.to_sym) do |value|
self.payload ||= {}
self.payload[attr_name.to_s] = value
end
end
def self.content_attributes
#content_attributes ||= {}
end
end
end
and, at my blog side, post.rb:
class Post < MyCms::Entry
content_attrs :body, :text
# Rest of the stuff...
end
I want the final command look something like this:
$ rails generate entry Post body:text
But I'm not quite sure I know how to implement it.
If you want to create custom model generator then you can use Thor (https://github.com/erikhuda/thor) which rails team has documented here
http://guides.rubyonrails.org/generators.html
First start by typing this command:
bin/rails g generator entry
So you are triggering generator to generate generator :). You will get something like this after running your command
create lib/generators/entry
create lib/generators/entry/entry_generator.rb
create lib/generators/entry/USAGE
create lib/generators/entry/templates
Then what you can do is just copy the code that ActiveRecord has for creating models. Inside your templates you will have this file model.rb like here
https://github.com/rails/rails/blob/master/activerecord/lib/rails/generators/active_record/model/templates/model.rb
You will see that it has something like this:
class <%= class_name %> < <%= parent_class_name.classify %>
class_name is a variable that you pass to your generator, like in your example Post. And you can define it like this inside your entry_generator
argument :class_name, type: :string, default: "Post"
I would definitely recommend that you look at this file since the rails generator has everything you need...you would just customize it for your specific need:
https://github.com/rails/rails/blob/master/activerecord/lib/rails/generators/active_record/model/model_generator.rb
In generator you have a class that invokes the template. Here is another video from Ryan Bytes that explains creating generator on custom layout which you can find it useful for your CMS.
http://railscasts.com/episodes/218-making-generators-in-rails-3?view=asciicast
Good luck
What you are trying to do is single-table inheritance (STI) and Rails has the support for that. Basically you need column type in your entries table which would store the class name within your hierarchy. You don't need to use the parent attribute as it seems to be in your example. See more in Rails docs or blogs, e.g. this one.

Rename existing rails model and add namespace

Original code looks like this:
# app/models/sso_configuration.rb
class SsoConfiguration < ActiveRecord::Base
end
# db/schema.rb
create_table "sso_configurations", force: true do |t|
...
end
I have to rename the model and add namespace so that I'll have Sso::SamlConfiguration. I changed model and database table.
# db/migrate20160225144615_rename_sso_configurations_to_sso_saml_configurations.rb
class RenameSsoConfigurationsToSsoSamlConfigurations < ActiveRecord::Migration
def change
rename_table :sso_configurations, :sso_saml_configurations
end
end
# db/schema.rb
create_table "sso_saml_configurations", force: true do |t|
...
end
# app/models/sso/saml_configuration.rb
module Sso
class SamlConfiguration < ActiveRecord::Base
end
end
When I open my rails console, the following happens.
> Sso::SamlConfiguration
=> Sso::SamlConfiguration(Table doesn't exist)
> Sso::SamlConfiguration.new
=> PG::UndefinedTable: ERROR: relation "saml_configurations" does not exist
My original thinking was that namespaced models should, by convention, have the snakecase name as the table name such that Foo::Bar should have a corresponding foo_bars table. Am I missing something with my setup?
rename_table :sso_configurations, :sso_saml_configurations
would imply this SsoSamlConfiguration.all when your trying to do this Sso::SamlConfiguration.all
Simply rollback your migration and change this line
rename_table :sso_configurations, :sso_saml_configurations
to this
rename_table :sso_configurations, :saml_configurations
and now this should work
Sso::SamlConfiguration.all
PG::UndefinedTable: ERROR: relation "saml_configurations" does not
exist
Rails by default looks for the table whose name is plural name of model i.e, in your case it looks for saml_configurations as the model name is saml_configuration.
You need to explicitly map the model to a different table by using self.table_name
# app/models/sso/saml_configuration.rb
module Sso
class SamlConfiguration < ActiveRecord::Base
self.table_name = "sso_saml_configurations"
end
end
I figured out the solution by copying what rails would do if I let it generate a namespaced model for me
rails g model sso/test
invoke active_record
create db/migrate/20160226074853_create_sso_tests.rb
create app/models/sso/test.rb
create app/models/sso.rb
invoke rspec
create spec/models/sso/test_spec.rb
invoke factory_girl
create spec/factories/sso_tests.rb
I checked all the path and name conventions in these new files and the only one I missed was the file app/models/sso.rb.
Creating the following solved my problem:
# app/models/sso.rb
module Sso
def self.table_name_prefix
'sso_'
end
end
Then
rails d model sso/test

Rails: change database model on production enviroment

i need to convert some data, but not sure how to do it.
I have a professional model, which had an foreign key. We decided that wasn't enough and changed the "has many" to a HABTM model, but now in the production environment, i need to convert the data from the foo_id field to the professional_foo joint table.
The "add table" migration will be executed before the "drop column" one, but how should i set up a conversion knowing that i have databases in use that use the old form and i will have new setups of the system that will be made straight to the last code version and because that, wont need any conversion to be done. (initializes scripts on newest version are already fixed.
I recommend doing the conversion of data in the migration between the add table and the drop column. This migration should be run when the new code is deployed so the new code will work with the new data structure and new installations that don't have any data yet will just run the migration very fast since there won't be data to convert.
The migration would look something like:
class OldProfessional < ActiveRecord::Base
self.table_name = "professionals"
has_many :foos
end
class NewProfessional < ActiveRecord::Base
self.table_name = "professionals"
has_and_belongs_to_many :foos
end
class MigrateFoosToHasAndBelongsToMany < ActiveRecord::Migration
def up
OldProfessional.all.each do |old_pro|
new_pro = NewProfessinoal.find(old_pro.id)
old_pro.foos.each do |foo|
new_pro.foos << foo
end
new_pro.save!
end
end
def down
NewProfessional.all.each do |new_pro|
old_pro = OldProfessional.find(new_pro.id)
new_pro.foos.each do |foo|
old_pro.foos << foo
end
old_pro.save!
end
end
end

How to cache tags with acts_as_taggable_on?

I have model with tag context:
class Product < ActiveRecord::Base
acts_as_taggable_on :categories
end
I'm trying to initialize tags caching:
class AddCachedCategoryListToProducts < ActiveRecord::Migration
def self.up
add_column :products, :cached_category_list, :string
Product.reset_column_information
products = Product.all
products.each { |p| p.save_cached_tag_list }
end
end
But cached_category_list does not initializing. What I'm doing wrong? Does anybody can use caching with this gem (my version is 2.0.6)?
Well, today I had the same problem.
I finally solved it, and my migration cached the desired tags.
The problem with your migration was two-fold:
The ActsAsTaggable code which sets up caching needs to run again after the column information is reset. Otherwise, the caching methods are not created (see https://github.com/mbleigh/acts-as-taggable-on/blob/v2.0.6/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb)
The method you are calling, save_cached_tag_list, does NOT automatically save the record, as it is installed as a before_save hook, and it doesn't want to create an infinite loop. So you must call save.
So, try replacing your migration with the following, and it should work:
class AddCachedCategoryListToProducts < ActiveRecord::Migration
def self.up
add_column :products, :cached_category_list, :string
Product.reset_column_information
# next line makes ActsAsTaggableOn see the new column and create cache methods
ActsAsTaggableOn::Taggable::Cache.included(Product)
Product.find_each(:batch_size => 1000) do |p|
p.category_list # it seems you need to do this first to generate the list
p.save! # you were missing the save line!
end
end
end
That should do it.
If you are using this in combination with owned tags, that might be the problem.
Looking at the code of the gem, it seems that the caching of owned tags isn't support
Hope this helps,
Best,
J

Add Rows on Migrations

I'd like to know which is the preferred way to add records to a database table in a Rails Migration. I've read on Ola Bini's book (Jruby on Rails) that he does something like this:
class CreateProductCategories < ActiveRecord::Migration
#defines the AR class
class ProductType < ActiveRecord::Base; end
def self.up
#CREATE THE TABLES...
load_data
end
def self.load_data
#Use AR object to create default data
ProductType.create(:name => "type")
end
end
This is nice and clean but for some reason, doesn't work on the lasts versions of rails...
The question is, how do you populate the database with default data (like users or something)?
Thanks!
The Rails API documentation for migrations shows a simpler way to achieve this.
http://api.rubyonrails.org/classes/ActiveRecord/Migration.html
class CreateProductCategories < ActiveRecord::Migration
def self.up
create_table "product_categories" do |t|
t.string name
# etc.
end
# Now populate the category list with default data
ProductCategory.create :name => 'Books', ...
ProductCategory.create :name => 'Games', ... # Etc.
# The "down" method takes care of the data because it
# drops the whole table.
end
def self.down
drop_table "product_categories"
end
end
Tested on Rails 2.3.0, but this should work for many earlier versions too.
You could use fixtures for that. It means having a yaml file somewhere with the data you want to insert.
Here is a changeset I committed for this in one of my app:
db/migrate/004_load_profiles.rb
require 'active_record/fixtures'
class LoadProfiles < ActiveRecord::Migration
def self.up
down()
directory = File.join(File.dirname(__FILE__), "init_data")
Fixtures.create_fixtures(directory, "profiles")
end
def self.down
Profile.delete_all
end
end
db/migrate/init_data/profiles.yaml
admin:
name: Admin
value: 1
normal:
name: Normal user
value: 2
You could also define in your seeds.rb file, for instance:
Grid.create :ref_code => 'one' , :name => 'Grade Única'
and after run:
rake db:seed
your migrations have access to all your models, so you shouldn't be creating a class inside the migration.
I am using the latest rails, and I can confirm that the example you posted definitely OUGHT to work.
However, migrations are a special beast. As long as you are clear, I don't see anything wrong with an ActiveRecord::Base.connection.execute("INSERT INTO product_types (name) VALUES ('type1'), ('type2')").
The advantage to this is, you can easily generate it by using some kind of GUI or web front-end to populate your starting data, and then doing a mysqldump -uroot database_name.product_types.
Whatever makes things easiest for the kind of person who's going to be executing your migrations and maintaining the product.
You should really not use
ProductType.create
in your migrations.
I have done similar but in the long run they are not guaranteed to work.
When you run the migration the model class you are using is the one at the time you run the migration, not the one at the time you created the migration. You will have to be sure you never change your model in such a way to stop you migration from running.
You are much better off running SQL for example:
[{name: 'Type', ..}, .. ].each do |type|
execute("INSERT INTO product_types (name) VALUES ('#{type[:name]} .. )
end

Resources