Rails 4: polymorphism, single-table inheritance, scoping...? - ruby-on-rails-4

Suppose the following:
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
end
Customers can be red customers and/or blue customers, but they are never just a customer. Red customers own dogs, blue customers wear hats (a customer must be hat-wearing or dog-owning). For a given order, red_customer is never equal to blue_customer. Since I never use customer_id, I want to eliminate it from my schema and replace it with red_customer_id and blue_customer_id. Each order has a red_customer_id and blue_customer_id.
Examples...
#some_order.customer # no, bad, do not want
#some_order.red_customer # returns a user object
#some_order.blue_customer # returns a user object
Schema (currently ugly)
create_table "orders"
t.string "name"
t.integer "user_id" # I want to get rid of this
t.integer "red_customer_id"
t.integer "blue_customer_id"
end
But if I remove user_id, then my associations break. How can I clean this up?

Assuming an order can only belong to a single user at a time I do not recommended to have 2 foreign_keys in your table. You can achieve the same result using Single Table Inheritance:
class Customer < ActiveRecord::Base
has_many :orders
end
class RedCustomer < Customer
end
class BlueCustomer < Customer
end
class Order < ActiveRecord::Base
belongs_to :red_customer, foreign_key: 'customer_id'
belongs_to :blue_customer, foreign_key: 'customer_id'
end
This way you keep a single table for customers and a single foreign_key on orders.
For this to work you need a type string column in the customer table, also as the Order migration, add polymorphic: true to the customer foreign_key declaration, this way the column customer_type will also be created.
class CreateCustomers < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :type
t.timestamps null: false
end
end
end
class CreateOrders < ActiveRecord::Migration
def change
create_table :orders do |t|
t.references :customer, polymorphic: true, index: true
t.timestamps null: false
end
end
end
It works as expected:
order = Order.create
order.red_customer = BlueCustomer.create #=> raises <ActiveRecord::AssociationTypeMismatch>
order.red_customer = RedCustomer.create #=> Ok
order.blue_customer #=> nil
order.red_customer #=> #<RedCustomer id: ...>
order.blue_customer = BlueCustomer.create #=> Ok
order.red_customer #=> nil
Hope this helps you.

Related

rails 4 migration one person multiple addresses: ActiveRecord error STI

I have a person model that i want to connect to multiple addresses (main_address, work_address etc.)
In app/models/person.rb:
class Person < ActiveRecord::Base
belongs_to :main_address, class_name: :Address, foreign_key: :main_address_id
belongs_to :work_address, class_name: :Address, foreign_key: :work_address_id
belongs_to :invoice_address, class_name: :Address, foreign_key: :invoice_address_id
belongs_to :further_address, class_name: :Address, foreign_key: :further_address_id
end
And an address model:
class Address < ActiveRecord::Base
belongs_to :person
end
address migration:
class CreateAddresses < ActiveRecord::Migration
def change
create_table :addresses do |t|
t.string :street
t.string :zip
t.string :city
t.string :country
t.string :lockbox
t.string :type
t.references :person, index: true, foreign_key: true
t.timestamps null: false
end
end
end
app/models/main_address.rb:
class MainAddress < Address
end
When i try to save an address in rails console (the person with id 1 was already created):
2.2.1 (main):0 > main_address = Address.create(type: "main_address", street: "Schumannstr.15", zip: "D-53113", city: "Bonn", country: "Germany", person_id: 1)
I get the error:
ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: main_address is not a subclass of Address
What am i doing wrong?
I think there may be a problem with your belongs_to associations. It seems to me that an address belong to a person, and a person has many addresses. If conceptually this does not represent your requirements then the rest of this answer will need to be adapted.
Also, in the original example you are specifying foreign keys that don't exist as columns in the addresses table. Each address will just be referred to through its id and type pair.
I would expect this type of code to work:
# app/models/person.rb
class Person < ActiveRecord::Base
has_many :addresses
end
# app/models/address.rb
class Address < ActiveRecord::Base
belongs_to :person
end
# app/models/main_address.rb
class MainAddress < Address
end
The type column in your table should trigger STI, allowing you to do something like:
person = Person.create(...)
person.addresses.create(street: "Schumannstr.15", zip: "D-53113", city: "Bonn", country: "Germany")
Hopefully this will work for you. If not, feel free to edit or comment as needed.

Can't get has_many & belongs_to in same model to work

It has been a while since I asked something around here.
Since I have just started playing around with Ruby, and can't get my head around this, I decided to come back here for some guidance.
I've read quite a few examples now, but I don't find a proper path to walk to solve this problem.
The idea is the following :
You have :
A family
A person
Their relationship is the following :
There is one person that is the 'head' of the family
Every person is part of a family (be it with multiple persons or just himself in it)
Now the current validation probably makes it impossible to perform this, but the idea is that there is no such thing as a family without a head, and no person without a family.
I've tried to express it the following way :
Family class
class Family < ActiveRecord::Base
validates :head_id, presence: true
belongs_to :head, :class_name => Person, foreign_key: 'head_id'
has_many :persons
end
Person class
class Person < ActiveRecord::Base
validates :family_id, presence: true
validates :first_name, presence: true
belongs_to :family
end
Would any soul be so kind to offer me some advice?
The migration has a family_id on the Person-class side, the family consists of an extra column being person_id.
Family migration
class CreateFamilies < ActiveRecord::Migration
def change
create_table :families do |t|
t.integer :head_id
t.timestamps null: false
end
end
end
Person migration
class CreatePersons < ActiveRecord::Migration
def change
create_table :persons do |t|
t.string :first_name
t.string :last_name
t.integer :age
t.integer :family_id
t.timestamps null: false
end
end
end
You got a circular dependency here. You cannot create families nor people.
Circular dependency usually indicates that there's a third element missing.
More eloquently and not completely related .
There are two kind of membership, head and non-head. Once your make them obvious in your code the circular dependency will be solved.
There a few solutions but you need to find the one fits you the best. This is mine.
Create a membership join table
class Family
validates :memberships, presence: true, on: :update
has_many :memberships
has_many :people, through: :membership
after_create :create_head_membership
private
def create_head_membership
memberships.create role: 'head' # to be filled later
end
end
class Membership
belongs_to :family
belongs_to :person
end
class Person
has_many :memberships
has_one :family, through: :membership # has_many, amrite?
validates :memberships, presence: true, on: :update
end
The most important thing is avoid Person/Family direct manipulation, rather create something to handle this process and wrap it in a transaction
class God
def make_family head_attrs
fam, head = nil
Family.transaction do
fam = Family.create!
head = Person.create! head_attrs
fam.memberships.first.update_attribute! :person_id, head.id
end
fam
end
end
I am not sure if I properly understand your code, but please tell me what is a Publisher class? I think you should do it in this way:
Family model:
has_many :persons
has_one :head, :class_name => Person, foreign_key: 'head_id'
validates :head_id, presence: true
Person model:
belongs_to :family
validates :family_id, presence: true
validates :first_name, presence: true
Family has many persons and has one head. The person belongs to family and it doesn't matter for the person if it's the head of family or not.
Other things like migrations and validations still the same.
Is better validate presence of :head that not head_id
class Family < ActiveRecord::Base
validates :head, presence: true
belongs_to :head, :class_name => Person, foreign_key: 'head_id'
has_many :persons
end
Person have two relations with Family, one if is head and another if is part of a family, you have to validate (as I put in the example) how you can handle this relationships.
class Person < ActiveRecord::Base
validates :family, presence: true
validates :first_name, presence: true
belongs_to :family
belongs_to :head, :class_name => 'Family', :foreign_key => 'head_id'
validate :family_id_is_equal_to_head_id
def family_id_is_equal_to_head_id
self.family = self.head
end
end
Finally migrations are wrong, must define the relantionship
class CreateFamilies < ActiveRecord::Migration
def change
create_table :families do |t|
t.integer :head_id
t.timestamps null: false
end
end
end
class CreatePersons < ActiveRecord::Migration
def change
create_table :persons do |t|
t.string :first_name
t.string :last_name
t.integer :age
t.belongs_to :family
t.integer :head_family_id
t.timestamps null: false
end
end
end

Rails model namespacing dependent destroy causes mysql error unknown field

I have several models in a separate folder called jira (instance.rb, generic_field.rb, etc.). They are are all namespaced under JIRA, for example JIRA::Instance < ActiveRecord::Base, JIRA::GenericField < ActiveRecord::Base. Here are the two models:
class JIRA::GenericField < ActiveRecord::Base
self.table_name = "jira_generic_fields"
belongs_to :jira_instance, class_name: JIRA::Instance
end
class JIRA::Instance < ActiveRecord::Base
self.table_name = "jira_instances"
has_many :jira_generic_fields, dependent: :destroy, class_name: JIRA::GenericField
end
The DB schema for the tables:
create_table "jira_generic_fields", force: true do |t|
t.string "jira_id"
t.string "name"
t.integer "jira_instance_id", null: false
end
create_table "jira_instances", force: true do |t|
t.string "jira_link"
t.string "crowd_link"
end
In my rails console i create a JIRA::Instance object and when I try to destroy it I get this:
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'jira_generic_fields.instance_id' in 'where clause': SELECT `jira_generic_fields`.* FROM `jira_generic_fields` WHERE `jira_generic_fields`.`instance_id` = 1
Why does ActiveRecord use jira_generic_fields.instance_id instead of jira_generic_fields.jira_instance_id and how can I fix this while keeping the models under the same JIRA namespace?
In the end specifying the foreign_key in the models solved this ...
class JIRA::GenericField < ActiveRecord::Base
self.table_name = "jira_generic_fields"
belongs_to :jira_instance, foreign_key: 'jira_instance_id', class_name: JIRA::Instance
end
class JIRA::Instance < ActiveRecord::Base
self.table_name = "jira_instances"
has_many :jira_generic_fields, dependent: :destroy, foreign_key: 'jira_instance_id', class_name: JIRA::GenericField
end
I don't really like it but it will have to do for now.

Polymorphic relationship - what is stored in the type attribute?

Let's say you have a polymorphic relationship like this:
class Picture < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
class Employee < ActiveRecord::Base
has_many :pretty_pictures, as: :imageable
end
class ProductInvoice < ActiveRecord::Base
has_many :pretty_pictures, as: :imageable
end
And this is your migration for the Picture model:
class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true
t.timestamps
end
end
end
Let's say you have a #product_invoice with an :id of 1 and you have a #picture that belongs to this product. I know that #picture.imagable_id should equal 1, but what the value stored in #picture.imagable_type be?
'ProductInvoice'
'ProductInvoices'
'product_invoice'
'product_invoices'
via G.B in comments
'ProductInvoice'

Displaying many-to-many using select multiple

right now im building a form that accepts a many to many using a select multiple. after creating the form, im trying to display the information collected in the SHOW page however i dont know how to display that data in embedded ruby. join model is CardTypesList
Models
class Card < ActiveRecord::Base
self.inheritance_column = nil
validates :name, presence: true, uniqueness: {case_sensitive: false}
has_many :card_type_lists
has_many :card_types, through: :card_type_lists
accepts_nested_attributes_for :card_type_lists
end
class CardType < ActiveRecord::Base
has_many :card_type_lists
has_many :cards, through: :card_type_lists
end
class CardTypeList < ActiveRecord::Base
belongs_to :cards
belongs_to :card_types
accepts_nested_attributes_for :card_type
end
NEW form using select
<%= f.label :types %>
<%= f.select :card_type_ids, CardTypes.all.collect{|x| [x.name, x.name]}, {},{:title => "Select a Type", :multiple => true, :class => 'selList'} %>
Embedded ruby trials
<td class="card-td"><%= #card.card_types %></td>
Expected: ["Type 1", "Type2"]
this renders the page, but yields (in text):
<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_CardTypes:0x00000102f58a18>
EDIT: schema added.
ActiveRecord::Schema.define(version: 20140120042152) do
create_table "card_type_lists", force: true do |t|
t.integer "card_type_id"
t.integer "card_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "card_types", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "cards", force: true do |t|
t.string "name"
t.string "set"
t.string "card_types"
t.string "colors"
t.string "cost"
t.string "rarity"
t.string "oracle"
t.float "value"
t.integer "number_owned"
t.string "notes"
t.string "img_link"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "types_mask"
end
add_index "cards", ["name"], name: "index_cards_on_name", unique: true
end
and this is my controller:
class CardsController < ApplicationController
def new
#card = Card.new
#card.card_type_lists.build.build_card_type
end
def show
#card = Card.find(params[:id])
end
def create
#card = Card.new(card_params)
if #card.save
redirect_to #card
else
render 'new'
end
end
private
def card_params
params.require(:card).permit(:name, :set, {:card_types => []}, :color, :cost, :rarity,:oracle,:value, :number_owned,:notes)
end
end
Should be:
class CardTypeList < ActiveRecord::Base
belongs_to :card
belongs_to :card_type #Singular
end
Some more things to think about:
Does your join model have the correct columns?
You should use accepts_nested_attributes_for
Look at how you're calling the data
Schema
Join models in has_many :through have to have foreign_key references to both models they're joining. The way you do this is to use a schema like this:
card_types_lists
id | card_id | card_type_id | other | information | created_at | updated_at
When you mentioned the error no such column: card_type_lists.card_types_id, it generally means you either don't have the correct column in the db, or your reference is incorrect. Looking at it, it's your association (referencing plural instead of singular)(fixed above)
Forms
Something you should consider is using accepts_nested_attributes_for to send the correct data to the nested models
This is when you want to create some records in either model, and works by allowing you to define "new" objects for your other models in your parent model, passing the data to your child models, like this:
#app/models/card.rb
Class Card < ActiveRecord::Base
has_many :card_type_lists
has_many :card_types, through: :card_type_lists
accepts_nested_attributes_for :card_type_lists
end
#app/models/card_type_list.rb
Class CardTypeList < ActiveRecord::Base
belongs_to :card
belongs_to :card_type
accepts_nested_attributes_for :card_type
end
#app/controllers/cards_controller.rb
def new
#card = Card.new
#card.card_types_lists.build.build_card_type
end
Data
If you want to show your associative data correctly, you should try this:
#app/views/cards/show.html.erb
<%= #card.card_types %>