ActiveRecord, has_many, polymorphic and STI - ruby-on-rails

I've came into a problem while working with AR and polymorphic, here's the description,
class Base < ActiveRecord::Base; end
class Subscription < Base
set_table_name :subscriptions
has_many :posts, :as => :subscriptable
end
class Post < ActiveRecord::Base
belongs_to :subscriptable, :polymorphic => true
end
in the console,
>> s = Subscription.create(:name => 'test')
>> s.posts.create(:name => 'foo', :body => 'bar')
and it created a Post like:
#<Post id: 1, name: "foo", body: "bar", subscriptable_type: "Base", subscriptable_id: 1, created_at: "2010-05-10 12:30:10", updated_at: "2010-05-10 12:30:10">
the subscriptable_type is Base but Subscription, anybody can give me a hand on this?

If the class Base is an abstract model, you have to specify that in the model definition:
class Base < ActiveRecord::Base
self.abstract_class = true
end

Does your subscriptions table have a 'type' column? I'm guessing that Rails thinks that Base/Subscription are STI models. So when a row is retrieved from the subscriptions table and no 'type' column is present, it just defaults to the parent class of Base. Just a guess...

Related

Redefine foreign key name for all associations in Rails

I have an STI model with many associations:
class MyModel < ActiveRecord::base
has_many :things
has_many :other_things
# ... a lot of `has_many`
end
Then I add non STI model as nested just to add some specific behaviour to MyModel without extending it directly:
class Nested < MyModel
self.inheritance_column = nil
end
But then my associations don't work. They have my_model_id column because they refer to MyModel and they should refer to Nested as well. But all these has_many's expect to use nested_id column as a foreign key (it depends on class name).
I could've typed inside class Nested:
has_many :things, foreign_key: 'my_model_id'
has_many :other_things, foreign_key: 'my_model_id'
But if it's possible, how to specify the foreign key for all the associations at once in Nested class?
If all your has_many associations are declared on MyModel, you should be fine, or you may benefit from upgrading Rails; the following works perfectly for me in 4.2.4. Rails uses the class that declares has_many to generate the foreign_key, so even when Nested inherits, :things still get looked up by my_model_id.
class MyModel < ActiveRecord::Base
has_many :things
end
class MyA < MyModel
end
class Nested < MyModel
self.inheritance_column = nil
end
class Thing < ActiveRecord::Base
belongs_to :my_model
end
> m = MyModel.create
=> #<MyModel id: 1, type: nil, created_at: "2015-12-22 02:21:16", updated_at: "2015-12-22 02:21:16">
> m.things.create
=> #<ActiveRecord::Associations::CollectionProxy [#<Thing id: 1, my_model_id: 1, created_at: "2015-12-22 02:21:19", updated_at: "2015-12-22 02:21:19">]>
> n = Nested.create
=> #<Nested id: 2, type: nil, created_at: "2015-12-22 02:21:27", updated_at: "2015-12-22 02:21:27">
> n.things.create
=> #<ActiveRecord::Associations::CollectionProxy [#<Thing id: 2, my_model_id: 2, created_at: "2015-12-22 02:21:32", updated_at: "2015-12-22 02:21:32">]>
> n.reload.things
Nested Load (0.2ms) SELECT "my_models".* FROM "my_models" WHERE "my_models"."id" = ? LIMIT 1 [["id", 2]]
Thing Load (0.1ms) SELECT "things".* FROM "things" WHERE "things"."my_model_id" = ? [["my_model_id", 2]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Thing id: 2, my_model_id: 2, created_at: "2015-12-22 02:21:32", updated_at: "2015-12-22 02:21:32">]>
If you're giving Nested its own has_many associations, the code that generates foreign keys is pretty deep and inaccessible. You end up in the HasManyReflection, which either uses the foreign_key (or as) option declared on your has_many or derives it from the class name. There's no obvious way to customize those methods unless you do something inadvisable like override Nested.name.
foreign_key:
def foreign_key
#foreign_key ||= options[:foreign_key] || derive_foreign_key
end
derive_foreign_key:
def derive_foreign_key
if belongs_to?
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
active_record.name.foreign_key
end
end
So the simplest way to go about this might just be a loop:
class Nested < MyModel
self.inheritance_column = nil
%i[things other_things].each do |association|
has_many association, foreign_key: 'my_model_id'
end
end
Or if you're feeling metaprogrammy, redefine has_many:
class Nested < MyModel
self.inheritance_column = nil
def self.has_many(klass, options={})
options.reverse_merge!(foreign_key: 'my_model_id')
super
end
has_many :things
has_many :other_things
end
My solution might be is not recommended, but it works. Here it is:
class Nested < MyModel
def self.name
MyModel.name
end
end
ActiveRecord will be looking for my_model_id foreign key for all the associations defined or redefined in Nested and MyModel classes.

How to prevent duplicate records in a Join Table

I'm quite new to Ruby and Rails so please bear with me.
I have two models Player, and Reward joined via a has_many through relationship as below. My Player model has an attribute points. As a player accrues points they get rewards. What I want to do is put a method on the Player model that will run before update and give the appropriate reward(s) for the points they have like below.
However I want to do it in such a way that if the Player already has the reward it won't be duplicated, nor cause an error.
class Player < ActiveRecord::Base
has_many :earned_rewards, -> { extending FirstOrBuild }
has_many :rewards, :through => :earned_rewards
before_update :assign_rewards, :if => :points_changed?
def assign_rewards
case self.points
when 1000
self.rewards << Reward.find_by(:name => "Bronze")
when 2000
self.rewards << Reward.find_by(:name => "Silver")
end
end
class Reward < ActiveRecord::Base
has_many :earned_rewards
has_many :players, :through => :earned_rewards
end
class EarnedReward < ActiveRecord::Base
belongs_to :player
belongs_to :reward
validates_uniqueness_of :reward_id, :scope => [:reward_id, :player_id]
end
module FirstOrBuild
def first_or_build(attributes = nil, options = {}, &block)
first || scoping{ proxy_association.build(attributes, &block) }
end
end
You should validate it in db also
Add follwing in migrate file-
add_index :earnedrewards, [:reward_id, :player_id], unique: true
EDIT:
I've realised that my previous answer wouldn't work, as the new Reward is not associated to the parent Player model.
In order to correctly associate the two, you need to use build.
See https://stackoverflow.com/a/18724458/4073431
In short, we only want to build if it doesn't already exist, so we call first || build
Specifically:
class Player < ActiveRecord::Base
has_many :earned_rewards
has_many :rewards, -> { extending FirstOrBuild }, :through => :earned_rewards
before_update :assign_rewards, :if => :points_changed?
def assign_rewards
case self.points
when 1000...2000
self.rewards.where(:name => "Bronze").first_or_build
when 2000...3000
self.rewards.where(:name => "Silver").first_or_build
end
end
class Reward < ActiveRecord::Base
has_many :earned_rewards
has_many :players, :through => :earned_rewards
end
class EarnedReward < ActiveRecord::Base
belongs_to :player
belongs_to :reward
validates_uniqueness_of :reward_id, :scope => [:reward_id, :player_id]
end
module FirstOrBuild
def first_or_build(attributes = nil, options = {}, &block)
first || scoping{ proxy_association.build(attributes, &block) }
end
end
When you build an association, it adds it to the parent so that when the parent is saved, the child is also saved. E.g.
pry(main)> company.customers.where(:fname => "Bob")
Customer Load (0.1ms) SELECT "customers".* FROM "customers"
=> [] # No customer named Bob
pry(main)> company.customers.where(:fname => "Bob").first_or_build
=> #<Customer id: nil, fname: "Bob"> # returns you an unsaved Customer
pry(main)> company.save
=> true
pry(main)> company.reload.customers
=> [#<Customer id: 1035, fname: "Bob">] # Bob gets created when the company gets saved
pry(main)> company.customers.where(:fname => "Bob").first_or_build
=> #<Customer id: 1035, fname: "Bob"> # Calling first_or_build again will return the first Customer with name Bob
Since our code is running in a before_update hook, the Player will be saved as well as any newly built Rewards as well.

Properly modeling monetary transactions in Rails 4

I am making a toy application to learn Rails 4 (without just cloning a tutorial).
Users sign up (I'm using the Devise gem to take care of user authentication), and a BTC pub/prv keypair is generated, and an address is computed and displayed to the user (in a flash message), so they can top off their account. Other Users sign up, and anyone can search for anyone and a dropdown is dynamically populated with every single user, but filters down names as a User types the name of their friend/associate, whoever they want to send Bitcoin to. I am only using testnet for this idea at the moment, no real BTC (don't worry!).
Anyways, here is my idea for modeling this application:
class User < ActiveRecord::Base
has_many :account
end
class Tx < ActiveRecord::Base
has_one :receiver, class => "account"
belongs_to :user, through :account
end
class Account < ActiveRecord::Base
belongs_to :user
has_many :tx
end
The reason why I don't like the above is because in my mind it seems that a Tx (short for transaction since transaction is a reserved word in Rails) actually belongs to two users, but my readings seem to indicate that I can't have something like this:
class User < ActiveRecord::Base
has_many :tx
end
class Tx < ActiveRecord::Base
has_one :receiver, class => "user"
has_one :sender, class => "user
end
Which of these implementations is better? I appreciate any insight into this model.
I'd go with the second method. I went with "transfers" instead of "tx", for readability - but you can name it as you please.
class User < ActiveRecord::Base
has_many :transfers
has_many :received_transfers, :class_name => "Transfer", :foreign_key => "receiver_id"
end
class Transfer < ActiveRecord::Base
belongs_to :user # Sender
belongs_to :receiver, :class => "User"
end
Testing it:
>> Transfer.create(:user_id => 1, :receiver_id => 2, :amount => 4.00)
=> #<Transfer id: 1, user_id: 1, receiver_id: 2, amount: #<BigDecimal:7fb3bd9ba668,'0.4E1',9(36)>, created_at: "2014-09-03 04:35:47", updated_at: "2014-09-03 04:35:47">
>> User.first.transfers
=> #<ActiveRecord::Associations::CollectionProxy [#<Transfer id: 1, user_id: 1, receiver_id: 2, amount: #<BigDecimal:7fb3c10682f0,'0.4E1',9(18)>, created_at: "2014-09-03 04:35:47", updated_at: "2014-09-03 04:35:47">]>
>> User.last.received_transfers
=> #<ActiveRecord::Associations::CollectionProxy [#<Transfer id: 1, user_id: 1, receiver_id: 2, amount: #<BigDecimal:7fb3bdabace8,'0.4E1',9(18)>, created_at: "2014-09-03 04:35:47", updated_at: "2014-09-03 04:35:47">]>
Happy coding!

rails basic polymorphism

i'm trying to create a polymorphic relationship between votes can be submitted by users and apply to articles. my code
class Vote < ActiveRecord::Base
attr_accessible :value, :voteable_id, :voteable_type
belongs_to :voteable, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :votes, :as => :voteable
end
class Article < ActiveRecord::Base
has_many :votes, :as => :voteable
end
<Vote id: 1, value: 1, created_at: "2012-07-27 03:13:14", updated_at: "2012-07-27 03:13:14", voteable_id: nil, voteable_type: nil>
From looking at the rails documentation via http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
I feel that my code is set correctly but i'm having a bit of trouble triggering it correctly, ie, how do I actually create a vote object with a properly define relationship to either an article or user?
Is votable_type is string?
Next example should work correctly..
#user.votes.new :value => 1
#user.save
.
I was able to make this work, I was setting the voteable_type attribute incorrectly.

Custom db entry for 3 way habtm in ROR

I am converting an existing perl gtk app into a ROR app
I have a 3 way habtm association model.
class CustomerMaster < ActiveRecord::Base
has_and_belongs_to_many :address_master, :join_table => "customer_phone_addres"
has_and_belongs_to_many :phone_master, :join_table => "customer_phone_address"
class AddressMaster < ActiveRecord::Base
has_and_belongs_to_many :customer_master, :join_table => "customer_phone_addres"
has_and_belongs_to_many :phone_master, :join_table => "customer_phone_addres"
class PhoneMaster < ActiveRecord::Base
has_and_belongs_to_many :customer_master, :join_table => "customer_phone_addres"
has_and_belongs_to_many :address_master, :join_table => "customer_phone_addres"
The join table has the following schema
CREATE TABLE customer_phone_address
(
id bigserial NOT NULL,
customer_master_id bigint,
phone_master_id bigint,
address_master_id bigint,
CONSTRAINT customer_phone_address_pkey PRIMARY KEY (id),
CONSTRAINT address FOREIGN KEY (address_master_id)
REFERENCES address_master (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT customer FOREIGN KEY (customer_master_id)
REFERENCES customer_master (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT phone FOREIGN KEY (phone_master_id)
REFERENCES phone_master (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT uniq_cust_phone_address UNIQUE (customer_master_id, phone_master_id, address_master_id)
)
And I have created a nested form for customer_master#new which takes inputs for both address_master and phone_master
Originally for the perl gtk app, only one entry is created in the join_table for each entry of customer, address and phone
id|customer_master_id|phone_master_id|address_master_id
186767|182774|500773|210683
However using the above relationship model, I get two entries in case of ROR
id|customer_master_id|phone_master_id|address_master_id
186769|182810|500775|nil|
186770|182810|nil|211935|
I need to maintain backward compatibility to the perl gtk app. How do I get that single entry in the join table instead of the two entries?
I think this question is similar to
https://stackoverflow.com/questions/5507150/custom-join-3-tables-usage-in-rails-2-3-8
Assume you are using Rails 3.
You should use habtm when your join table has only 2 foreign keys. For most cases, has_many :through will be more flexible.
In your case, you should create a model for the join table. First, you should disable pluralization for legacy DB schema. (I thought you already did that).
# In config/application.rb
config.active_record.pluralize_table_names = false
Create a join table, namely CustomerPhoneAddress by convention (I will give it a more meaningful name):
# customer_phone_address.rb
class CustomerPhoneAddress < ActiveRecord::Base
belongs_to :address_master
belongs_to :customer_master
belongs_to :phone_master
end
Finally, associate your models with has_many and has_many :through:
# address_master.rb
class AddressMaster < ActiveRecord::Base
has_many :customer_phone_addresses
has_many :customer_masters, :through => :customer_phone_addresses
has_many :phone_masters, :through => :customer_phone_addresses
end
# customer_master.rb
class CustomerMaster < ActiveRecord::Base
has_many :customer_phone_addresses
has_many :address_masters, :through => :customer_phone_addresses
has_many :phone_masters, :through => :customer_phone_addresses
end
# phone_master.rb
class PhoneMaster < ActiveRecord::Base
has_many :customer_phone_addresses
has_many :customer_masters, :through => :customer_phone_addresses
has_many :phone_masters, :through => :customer_phone_addresses
end
I tested the code and it works very well. To create an association:
CustomerPhoneAddress.create(
:phone_master => PhoneMaster.first,
:address_master => AddressMaster.first,
:customer_master => CustomerMaster.first)
To query the association:
IRB> a = AddressMaster.first
=> #<AddressMaster id: 1, created_at: "2011-12-07 15:23:07", updated_at: "2011-12-07 15:23:07">
IRB> a.customer_masters
=> [#<CustomerMaster id: 1, created_at: "2011-12-07 15:23:15", updated_at: "2011-12-07 15:23:15">]
IRB> a.phone_masters
=> [#<PhoneMaster id: 1, created_at: "2011-12-07 15:23:19", updated_at: "2011-12-07 15:23:19">]
IRB> a.customer_phone_addresses
=> [#<CustomerPhoneAddress id: 1, address_master_id: 1, customer_master_id: 1, phone_master_id: 1, created_at: "2011-12-07 15:24:01", updated_at: "2011-12-07 15:24:01">]

Resources