DelegatedType Inheritance - ruby-on-rails

I have two nearly identical models, except one has three or four extra attributes.
I didn't want to use Single Table Inheritance (STI) because instances of model A will have a bunch of nil values that only pertain to model B.
I didn't want to use abstract classes because I can't query across both, and the shared fields are duplicated in each table.
So I thought maybe Delegated Types introduced in rails 6.1 would be a good fit for this. So here is my contrived example:
Automobile holds all the shared fields, LuxuryAutomobile has some extra attributes that only pertain to it.
Migrations:
create_table "automobiles" do |t|
t.integer "user_id"
t.integer "num_wheels"
t.integer "num_doors"
t.string "color"
t.string "autoable_type"
t.integer "autoable_id"
t.references :autoable, polymorphic: true
end
create_table "luxury_automobiles" do |t|
t.boolean "leather_seats"
t.integer "num_screens"
t.string "custom_monogram"
end
create_table "users" do |t|
t.string "name"
end
automobile.rb
class Automobile < ApplicationRecord
belongs_to :user
delegated_type :autoable, types: %w[ Automobile LuxuryAutomobile ]
include Autoable
end
luxury_automobile.rb
class LuxuryAutomobile < ApplicationRecord
include Autoable
delegate :num_wheels, to: :automobile
end
autoable.rb
module Autoable
extend ActiveSupport::Concern
included do
has_one :automobile, as: :autoable, touch: true
end
end
So it's pretty close! I can create Automobiles and LuxuryAutomobiles
u = User.first
Automobile.create! autoable: LuxuryAutomobile.new(custom_monogram: "BMW"), user: u, num_wheels: 6
So I can then call:
la = LuxuryAutomobile.last
la.automobile.num_wheels #returns 6
I can shorten that call further by using delegate in luxury_automobile.rb
delegate :num_wheels, to: :automobile
and in the console:
la = LuxuryAutomobile.last
la.num_wheels #returns: 6
la.custom_monogram #returns: "BMW"
Is there a more seamless way to do this? I feel like it's 90% there, but I can't assign attributes like num_wheels to a LuxuryAutomobile. It would be great if missing attributes from LuxuryAutomobile looked up the chain acted as one with Automobile.
la = LuxuryAutomobile.last
la.num_wheels #returns: 6
la.custom_monogram #returns: "BMW"
la.num_wheels = 8 #NoMethodError (undefined method `num_wheels=' for #<LuxuryAutomobile:0x00007fa1e70fe750>)

Related

Nearly identical methods in PagesController produce an undefined method error in Rails 4

I have three tables in my Rails 4 app -- one for Game, Category, and Topic. Both Category and Topic have a column for :name, while Game includes information like starts_at for when a game begins.
In my PagesController, I can show data from both Game and Topic by using find_by with the params value:
topic = Topic.find_by_name(params[:topic])
#games = Game.for_topic(topic).upcoming.order(:starts_at)
This works fine.
What's weird is that when I use the same reasoning but with Category instead of Topic, like so:
category = Category.find_by_name(params[:category])
#games = Game.for_category(category).upcoming.order(:starts_at)
I receive an error message:
undefined method `for_category'
This is confusing to me since I am definitely defining category and the using it in my for_ expression. Am I making an error in my thinking?
Additional
CreateCategories Migration
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories do |t|
t.belongs_to :topic, index: true
t.string :name, :null => false
t.timestamps
end
end
end
CreateTopics Migration
class CreateTopics < ActiveRecord::Migration
def change
create_table :topics do |t|
t.string :name, :null => false
t.timestamps
end
end
end
I think you setup the named scope for_topic in the Game model. But is missing the for_category, which is why it is failing.
Try setting the named scope for_category in Game model.

Rails inherits subclass

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.

Relations not working as expected

This will be fairly quick and easy for most of you...I have a table called types, and another called projects. A project can only have one type, but a type can have many projects. For instance a community garden project and a playground project can both have the type of 'greenspace'. So I have set up a has_many association. In my types model I have this:
has_many :projects
and in my projects model I don't have anything (I previously had has_one in it but upon looking at the docs it seemed incorrect). In the projects#show view I would like the name of the type to display. The parks project's view should say 'greenspace'. but I am getting the error
undefined method `type' for #<Project:0x007ffdd14fcde8>
I am trying to access that name using:
<h3>Type: <%= #project.type.project_type %> </h3>
i have also tried:
<h3>Type: <%= #project.type_id.project_type %> </h3>
but of course type_id gives a number, and there is no project_type for a number. project_type being the name of the column which holds the string data 'greenspace'. Am I accessing it wrong? Or have I set it up incorrectly?
Also in my schema, projects looks like this:
create_table "projects", force: :cascade do |t|
t.string "type_id"
t.text "description"
t.integer "money_needed"
t.integer "money_raised"
t.float "interest_offered"
t.datetime "end_date"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
t.text "url"
end
Project can belong_to both. Like this
#app/models/project.rb
class Project < ActiveRecord::Base
belongs_to :type
belongs_to :user
#...
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :projects
#...
end
#app/models/type.rb
class Type < ActiveRecord::Base
has_many :projects
#...
end
In the Project model you should state:
belongs_to => :type
In general, for most associations there is going to be an inverse. Not always, as you might have multiple associations in Type for Project. For example as well as your current has_many :projects, you might have others to return only projects that are unfinished, and such an association would not need an inverse.
Bear in mind that when you state: #project.type Rails is going to look for a method on #project. The association is what provides this method, and effectively the result is then the Type object that is referenced by the Project. It's important to realise that #project.type only returns a Type because the association tells it to -- the magic does not extent to just inferring that that is what is wanted.

Custom foreign_key in model gives PG::Error column does not exist - Rails

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)

How to create a rails habtm that deletes/destroys without error?

I created a simple example as a sanity check and still can not seem to destroy an item on either side of a has_and_belongs_to_many relationship in rails.
Whenever I try to delete an object from either table, I get the dreaded NameError / "uninitialized constant" error message.
To demonstrate, I created a sample rails app with a Boy class and Dog class. I used the basic scaffold for each and created a linking table called boys_dogs. I then added a simple before_save routine to create a new 'dog' any time a boy was created and establish a relationship, just to get things setup easily.
dog.rb
class Dog < ActiveRecord::Base
has_and_belongs_to_many :Boys
end
boy.rb
class Boy < ActiveRecord::Base
has_and_belongs_to_many :Dogs
def before_save
self.Dogs.build( :name => "Rover" )
end
end
schema.rb
ActiveRecord::Schema.define(:version => 20100118034401) do
create_table "boys", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "boys_dogs", :id => false, :force => true do |t|
t.integer "boy_id"
t.integer "dog_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "dogs", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
end
I've seen lots of posts here and elsewhere about similar problems, but the solutions are normally using belongs_to and the plural/singular class names being confused. I don't think that is the case here, but I tried switching the habtm statement to use the singular name just to see if it helped (with no luck). I seem to be missing something simple here.
The actual error message is:
NameError in BoysController#destroy
uninitialized constant Boy::Dogs
The trace looks like:
/Library/Ruby/Gems/1.8/gems/activesupport-2.3.4/lib/active_support/dependencies.rb:105:in const_missing'
(eval):3:indestroy_without_callbacks'
/Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/callbacks.rb:337:in destroy_without_transactions'
/Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:229:insend'
...
Thanks.
I don't see your destroy callback, but I do see a couple of problems. First, your associations need to be lowercase. So dog.rb should be:
class Dog < ActiveRecord::Base
has_and_belongs_to_many :boys
end
and boy.rb should be:
class Boy < ActiveRecord::Base
has_and_belongs_to_many :dogs
def before_save
self.dogs.build( :name => "Rover" )
end
end
Second, I believe you want to use self.dogs.create instead of self.dogs.build above, since build won't actually save the new dog object.
The accepted answer here solved my problem, only to create another one.
Here are my model objects:
class Complex < ActiveRecord::Base
set_table_name "Complexes"
set_primary_key "ComplexID"
has_and_belongs_to_many :amenities
end
class Amenity < ActiveRecord::Base
set_table_name "Amenities"
set_primary_key "AmenityID"
end
Rails uses the name of the association as the table name when creating the select query. My application runs on Unix against a legacy MySQL database and my table names are case-sensitive and don't conform to Rails conventions. Whenever my app actually tried to load the association, I would get an exception that MySQL couldn't find table amenities:
SELECT * FROM `amenities`
INNER JOIN `ComplexAmenities` ON `amenities`.AmenityID = `ComplexAmenities`.AmenityID
WHERE (`ComplexAmenities`.ComplexID = 147 )
I searched and searched and could not find a way to tell Rails to use the correct case for the table name. Out of desperation, I tried passing a :table_name option to habtm and it worked. My new Complex model looks like this:
class Complex < ActiveRecord::Base
set_table_name "Complexes"
set_primary_key "ComplexID"
has_and_belongs_to_many :amenities, :table_name => 'Amenities'
end
This works under Rails 2.3.5.
This option is not mentioned in the Ruby on Rails docs.

Resources