I have problem with creating proper inheritance between classes in Ruby On Rails.
Idea:
There are 2 classes: Person and Client. Person is a abstract class and Client inherits Person attribute.
Problem:
My solution doesn't work. I don't know why. How can I correctly implement (prefer CTI) inheritance.
Migrations:
create_persons.rb
class CreatePersons < ActiveRecord::Migration
def self.up
create_table :persons do |t|
t.string :pesel, null: false
t.string :first_name, null: false
t.string :last_name, null: false
t.string :email, null: false
t.date :data_of_birth, null: false
t.string :password_digest, null: false
# required for STI
t.string :type
t.timestamps null: false
end
end
def self.down
drop_table :persons
end
end
create_clients.rb
class CreateClients < ActiveRecord::Migration
def change
create_table :clients do |t|
add_foreign_key :persons
t.timestamps null: false
end
end
end
Model Person
class Person < ActiveRecord::Base
self.abstract_class = true
end
Model Client
class Client < Person
end
After db:migrate, when I try Client.create(pesel: "1232",....) there is error:
unknown attribute 'pesel' for Client.
You're getting an error because you've created a clients table in addition to your persons table. If you have a separate table for each class then the only thing that is inherited is the code and not the contents of the database.
Single Table Inheritance (STI) allows you to add a type column and then instances of the parent class and subclasses will be stored in that single table provided the expected table for the subclass isn't present. You've added that type column but you've also created a clients table. This means that ActiveRecord is expecting you to store Client instances in that table instead of the persons - and when it tries to store a Client there it cannot use the field pesel causing your error.
So, if you want to use STI, then you need to remove your clients table and add an client-specific fields to your persons table.
Alternatively, you can keep your clients table and add the fields from persons that you want to also use for clients to the clients table. This wouldn't then be STI but your Client objects would inherit the methods from Person.
I suspect from your inclusion of the type field that you want to get rid of the clients table.
Single Table Inheritance is based upon having a single table to store all the records.
Your problem is that you create a table 'clients' which Rails uses by default for the Client class.
Just rake db:rollback on your last migration and it should look for the superclass table 'people' and work fine.
Edit : Oops, didn't see you mention CTI, this solution only works for STI.
Related
I am trying to get a handle on how to use foreign keys in Rails,
$ rails g scaffold categories cat:string value:integer
$ rails db:migrate
Then create a new table with a foreign key connecting to the first table categories,
$ rails g scaffold subcategories subcats:string subcatsvalue:integer categories:references
$ rails db:migrate
Then I append /categories to the url and the form is there as expected and I can do all CRUD operations.
Then I append /subcategories to the url and try to add some data to the form such as,
Subcats: blah
Subcatsvalue: 123
Categories: cat1
should this be the id of the category or the name of the category?
/RubyLearningApp/db/migrate/20200413195730_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.0]
def change
create_table :categories do |t|
t.string :cat
t.integer :value
t.timestamps
end
end
end
/RubyLearningApp/db/migrate/20200413200303_create_subcategories.rb
class CreateSubcategories < ActiveRecord::Migration[5.0]
def change
create_table :subcategories do |t|
t.string :subcats
t.integer :subcatsvalue
t.references :categories, foreign_key: true
t.timestamps
end
end
end
Is this correct way to set up a foreign key between tables?
When I fill in the Categories with 'cat1' I get the following error,
Schema.rb
ActiveRecord::Schema.define(version: 20200413200303) do
create_table "categories", force: :cascade do |t|
t.string "cat"
t.integer "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "subcategories", force: :cascade do |t|
t.string "subcats"
t.integer "subcatsvalue"
t.integer "categories_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["categories_id"], name: "index_subcategories_on_categories_id"
end
end
The model files:
category.rb
class Category < ApplicationRecord
end
subcategory.rb
class Subcategory < ApplicationRecord
belongs_to :categories
end
Any help would be greatly appreciated,
Thanks,
There are a few things wrong with your scaffolds that may be causing the problems. The correct way to generate a scaffold is to use a singular scaffold name:
rails g scaffold Category
and
rails g scaffold SubCategory
This will use Rails built in Inflector to pluralize the names where necessary.
When using references, you should also use the singular:
category:references
This is the Rails Way and it will sort out most of the problems you are having. The other issue is if you want to add the category to the url, you should nest your routes:
resources :categories do
resources :sub_categories
end
This will allow you to use routes like
http://localhost:3000/categories/1/subcategories
and
http://localhost:3000/categories/1/subcategories/1
The first number (the one closest to the left) is the category id and can be access by using params[:category_id] in the sub_categories_controller.rb file. The second number (the one closest to the right) is the sub_category id and can be accessed by params[:id] in the sub_categories_controller.rb file.
Well, after spending two days stuck figuring out how to solve the foreign key issue in Rails 6+ - even though i read a lot of comments from S.O which did not do much help. I finally found the solution.
Using add_reference in your migration, you can easily solve this.
Let's pick it up from where you have model files untouched and Rails generated.
For your Category Model, you should have:
class Category < ApplicationRecord
has_many :subcategories, foreign_key: :categories_id
end
And for your Subcategory Model, you should have:
class SucCategory < ApplicationRecord
belongs_to :category. foreign_key: :categories_id
end
This creates an Association atrribute that tells rails that a Category has many Subcategories that can be identified in the categories table by a foreign key found in the subcategories table known as categories_id
Then in your console, now run the command rails generate migration AddSubcategoriesToCategories to create a migration file. Within the generated migration file, be sure to have the change method;
class AddSubcategoriesToCategories < ActiveRecord::Migration[6.0]
def change
add_references :categories, :categories, references: :subcategories, type: :integer, index: false
end
end
This would create a categories_id column in your categories table and tells ActiveRecord to reference the values(s) from the subcategories table, automatically making it a foreign key.
Funny enough, the reason why the option :categories appears a second time is because ActiveRecord by default, looks for the column named id within the table from which the foreign key is taken - as it is the default index on creating tables. But as a different column with a different name is defined as the index, you will have to specify the name of the column (eg. keyname) in the add_reference function to make ActiveRecord append the phrase _id to what you just defined as the column name and find that column - now named 'keyname_id', else you'll receive errors that specify that the column 'id' referenced in foreign key constraint does not exist or if you specify the full column name as 'keyname_id' in your add_reference function, you'll receive errors that specify that the column 'keyname_id' referenced in foreign key constraint does not exit
So in this case the second :categories in the function is the first part of the name of the column to which ActiveRecord appends the remaining part '_id' to become :categories_id.
Drawback: All your foreign keys would then have to be snakecased as 'whateverkeyname_id' in your tables
I've got a Rails app where Users are able to keep track of airing shows and episodes.
To simplify the process of keeping track of (not yet) watched shows, users are able to synchronize their account with other services. They can, in their user settings page, choose which service they want to synchronize with.
To synchronize, I load their profile from the other service, and then run it through an algorithm which detects changes from the last synchronization, and updates the DB accordingly. In order to store the last synchronization status, for each Show ID, I create a "UsersSyncIdStatus" object which stores the show ID, as well as the current status for that show in the synchronized service.
Note that the services do not use the same Show IDs as my website, which means that I have a table which I can use to "convert" from their show IDs to my show IDs. Since the information each service provides is different, they must be stored in different tables.
Right now, this is (a simplified version of) how the DB schema is set up:
create_table "service1_ids", force: :cascade do |t|
t.integer "service_id", null: false
t.integer "show_id", null: false
[...]
end
create_table "service2_ids", force: :cascade do |t|
t.integer "service_id", null: false
t.integer "show_id", null: false
[...]
end
create_table "users_sync_id_statuses", force: :cascade do |t|
t.integer "user_id"
t.integer "service_id", null: false
t.integer "sync_status", default: 0, null: false
t.datetime "sync_date", null: false
[...]
end
create_table "users", force: :cascade do |t|
[...]
t.datetime "synced_at"
t.boolean "sync_enabled", default: false, null: false
t.integer "sync_method", default: 0, null: false
[...]
end
In particular, users.sync_method is an enum which stores the service the user has selected for synchronization:
SYNC_METHODS = {
0 => {
symbol: :service1,
name: 'Service1',
model_name: 'Service1Id',
show_scope: :service1_ids
}
1 => {
symbol: :service2,
name: 'Service2',
model_name: 'Service2Id',
show_scope: :service2_ids
}
}
This means I can easily know the model name of the IDs of a specific user by just doing SyncHelper::SYNC_METHODS[current_user.sync_method][:model_name].
Now, the question is, how can I have a relationship between "users_sync_id_statuses" and "serviceN_ids"? In order to know which class the "service_id" column corresponds to, I have to 'ask' the user model.
I currently have it implemented as a method:
class User
def sync_method_hash
SyncHelper::SYNC_METHODS[self.sync_method]
end
def sync_method_model
self.sync_method_hash[:model_name].constantize
end
end
class UsersSyncIdStatus
def service_id_obj
self.user.sync_method_model.where(service_id: self.service_id).first
end
end
However, UsersSyncIdStatus.service_id_obj is a method, not a relationship, which means I cannot do all the fancy stuff a relationship allows. For example, I cannot easily grab the UsersSyncIdStatus for a specific user and show ID:
current_user.sync_id_statuses.where(service_id_obj: {show_id: 123}).first
I could turn it into a polymorphic relationship, but I really don't want to have to add a text column to contain the class name, when it is a "constant" from the point of view of each user (for a user to switch synchronization service, all UsersSyncIdStatuses for that user are destroyed, so a user never has more than 1 service type in their UsersSyncIdStatuses).
Any ideas? Thank you in advance!
I don't think vanilla Rails 5 supports what I want to do, someone please correct me if I'm wrong.
Still, after some research into how Rails implements polymorphic relationships, I was able to relatively easily monkey-patch Rails 5 to add this functionality:
config/initializers/belongs_to_polymorphic_type_send.rb:
# Modified from: rails/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
module ActiveRecord
# = Active Record Belongs To Polymorphic Association
module Associations
class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
def klass
type = owner.send(reflection.foreign_type)
type.presence && (type.respond_to?(:constantize) ? type.constantize : type)
end
end
end
end
app/models/users_sync_id_status.rb:
class UsersSyncIdStatus
belongs_to :service_id_obj, polymorphic: true, foreign_key: :service_id, primary_key: :service_id
def service_id_obj_type
self.user.sync_method_model
end
end
With this monkey-patch, belongs_to polymorphic associations do not assume that the type field is a varchar column, but instead call it as a method on the object. This means you can very easily add your own dynamic behavior, without breaking any old behavior (AFAIK, didn't do intensive testing on that).
For my specific use-case, I have the sync_id_obj_type method query the user object for the class that should be used in the polymorphic association.
I have this three classes user, driver, company.
every company or driver belongs a user. The models look like
class Company < User
has_many :driver
end
class Driver < User
end
class User < ActiveRecord::Base
enum role: [:admin, :support, :B2B , :B2C]
end
and the database looks like
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
t.timestamps null: false
end
end
end
class CreateCompanies < ActiveRecord::Migration
def change
create_table :companies do |t|
t.string :comp_name
t.string :first_name_counterpart
t.string :last_name_counterpart
t.string :iban_nr
t.string :bic
t.string :email_counterpart
t.string :addresse
t.string :city
t.string :zip
t.references :user
t.timestamps null: false
end
end
end
class CreateDrivers < ActiveRecord::Migration
def change
create_table :drivers do |t|
t.string :first_name
t.string :last_name
t.date :birthday
t.integer :sex
t.integer :dpi
t.integer :score
t.references :user
t.timestamps null: false
end
end
end
Why can't I create a Driver-instance. For example, if I try d = Driver.new, I get a user-instance.d = Driver.new
=> #<Driver id: nil, email: nil, created_at: nil, updated_at: nil>
This is how Rails guesses the table name from the model classes. Quoting from the ActiveRecord docs for table_name:
Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used to guess the table name even when called on Reply.
You should be able to force the proper table name by the table_name= setter, e.g.:
class Driver < User
self.table_name = "drivers"
end
On the other hand, I am also not sure that your approach (with such inheritance) will not cause problems somewhere else.
If you have models with inheritance like you do:
class User < ActiveRecord::Base
enum role: [:admin, :support, :B2B , :B2C]
end
class Company < User
has_many :driver
end
class Driver < User
end
rails infers that you are after Single Table Inheritance (STI) and expects there is just a base table users with a column type which stores the records of User, Company and Driver with actual class name (ex: Company or Driver etc).
If you would rather want to have separate tables users, companies and drivers because each of those tables have different set of columns, and the only reason why you are put inheritance in place is to share some common functionality, then you should extract the common functionality into modules and mix them into those models (by just inheriting from ActiveRecord::Base.
rails, through active_support provides whats called concerns to extract the common functionality into modules and mix them intuitively.
You could probably get away with inheritance and still have these models point to separate tables with the declaration of self.table_name = "table_name". But it is not a good idea, as it goes around the rails conventions and may cause problems down the lane.
Refer to ActiveRecord::Inheritance and ActiveSupport::Concern for more info.
I have a model named PaypalPayment:
class PaypalPayment < PaymentMethod
belongs_to :order
def provider_class
PaypalPayment
end
def process!
end
end
I generated the following migrations for it:
class CreatePaypalPayments < ActiveRecord::Migration
def change
create_table :paypal_payments do |t|
t.integer :order_id
t.integer :payment_id
t.timestamps
end
end
end
and
class AddDetailsToPaypalPayment < ActiveRecord::Migration
def change
add_column :paypal_payments, :state, :string
add_column :paypal_payments, :amount, :decimal
add_column :paypal_payments, :cc, :string
add_column :paypal_payments, :cm, :string
end
end
After the migration the table looks something like:
development_database=# select * from paypal_payments;
id | order_id | payment_id | created_at | updated_at | state | amount | cc | cm
But when I try to initialize an object of this model, I'm getting the unknown attribute: payment_id.
#paypal_payment = PaypalPayment.new(:payment_id => params[:tx], :state => params[:st], :cc => params[:cc], :cm => params[:cm], :order_id => params[:id])
EDIT: db/schema.rb:
create_table "paypal_payments", :force => true do |t|
t.integer "order_id"
t.integer "payment_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "state"
t.decimal "amount"
t.string "cc"
t.string "cm"
end
There are different ways to model inheritance in a relational database, Martin Fowler lists the following options:
Single Table Inheritance : all classes are stored in a single table
Class Table Inheritance : all classes have their own table
Concrete Table Inheritance : only concrete classes have a table (e.g. in your example PaymentMethod if being abstract, would not have a table)
Now ActiveRecord only supports STI: single table inheritance.
So if you write
class PaypalPayment < PaymentMethod
ActiveRecord will assume STI and look for a type column, and furthermore, will only look for payment_methods table.
Depending on what you want, in most cases, STI is just perfect. Sometimes I prefer the Class and Concrete Table Inheritance better, but especially for associations this needs a little more householding, since:
e.g. you have different payment-methods, but they are stored in different tables
do you want to access all payment methods at once, you need the "abstract class"
you need an association per possible payment-method
if you have the "abstract class", how do you link to the "real payment method". One way is to include table-name and id of the child as the link.
There are lots of way to solve this, but always harder than using a single table. Also this is stretching the relational datamodel, as depending on the chosen solution, foreign key constraints are not automatically supported. I could go into detail,
but I am not sure if this is relevant, as your example seems a classic case for STI.
If you do want to use Class Table Inheritance or Concrete Table Inheritance, each class has to derive from `ActiveRecord::Base`` and you should include a module (or concern) with the shared behaviour if needed (since ruby does not support multiple inheritance).
I believe you have to add the column "type" to your PaymentMethods table. This will allow it to be inheritable. Without the type column, when you instantiate a PaypalPayment, it thinks it's a PaymentMethod and hence has none of the unique fields of PaypalPayment. However when you add the column "type" to PaymentMethod, then it will store "PaypalPayment" and ActiveRecord knows to make the PaypalPayment methods available. You should probably make a model for PaymentMethod also and make sure it inherits ActiveRecord::Base
def change
add_column :payment_methods, :type, :string
end
Here's some info:
http://www.archonsystems.com/devblog/2011/12/20/rails-single-table-inheritance-with-polymorphic-association/
I'd do this:
Check your Rails Console --
$ rails c
$ payment = PaypalPayment.find(1)
$ payment.column_names #-> should reveal which columns Rails comes back with
Check Rails is picking up the attribute
For testing's sake, just try attr_accessor :payment_id to see if that works. You might not have permitted the attribute in your model
In Rails4, that means using strong params, but in Rails 3, I think it means using attr_accessible like this:
#app/models/paypal_payment.rb
Class PaypalPayment < ActiveRecord::Base
attr_accessible :payment_id #-> tests parameter passing
attr_accessor :payment_id #-> tests virtual attribute assignment
end
I know I'm a bit late the show here, but if anyone is encountering a similar problem with Rails 5.1, in my case I was able to resolve the issue by including the following line in my parent classes
self.abstract_class = true
I have a VideoCollection model that will contain many records from another model (called VideoWork), using the has_many relationship. The VideoCollection model inherits from the Collection model using single table inheritance, while the VideoWork model inherits from the Work model.
I'm having a problem when I try to call up the video_works that belong to a video_collection.
In my video_collection#show action, I use the following to try to display a collection's works:
def show
#video_collection = VideoCollection.find(params[:id])
#collections = #video_collection.children
#works = #video_collection.video_works
end
But when I try to use #works in the show view, I get the following:
PG::Error: ERROR: column works.video_collection_id does not exist
SELECT "works".* FROM "works" WHERE "works"."type" IN ('VideoWork') AND "works"."video_collection_id" = $1
##(Error occurs in the line that contains <% #works.each do |work| %>)
My model files:
#----app/models/video_collection.rb----
class VideoCollection < Collection
has_many :video_works
end
#----app/models/video_work.rb----
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
The "parent" models:
#----app/models/collection.rb - (VideoCollection inherits from this)
class Collection < ActiveRecord::Base
end
#----app/models/work.rb - (VideoWork inherits from this)
class Work < ActiveRecord::Base
end
The Schema file:
#----db/schema.rb----
create_table "works", force: true do |t|
t.string "header"
t.string "description"
t.string "type"
t.string "folder_id"
end
create_table "collections", force: true do |t|
t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
t.text "ancestry"
t.string "name"
t.string "tile_image_link"
end
My Question
I assume that since I have a folder_id column in the works table that I should be able to set up the belongs_to relationship properly, but it seems that Rails still wants me to have a video_collection_id column instead. I would prefer not use something specific like video_collection_id as a foreign key in the works table since I need to set up other relationships (e.g.: photo_collection has_many photo_works, etc).
What am I doing wrong here?
I don't really use has_many and belongs_to with different foreign keys than the standard, but according to the docs I would do this:
class VideoCollection < Collection
has_many :video_works, foreign_key: "folder_id"
end
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
Your Pg error says that the association is looking for 'video_collection_id' instead of 'folder_id'
Guides (chapter 4.3.2.5)