i have a question about associations in rails. The situation is the following:
Models:
class Character < ActiveRecord::Base
has_one :character_stats
end
class CharacterStats < ActiveRecord::Base
belongs_to :character
end
Now i need to create stats when a new character is created.
What i doo is this at the moment, i feel like this is a workaround with rails. Is there a more "raily" way to do this?
after_save :character_init
def character_init
create_stats
end
def create_stats
stats = CharacterStats.new
stats.character_id = self.id // this bothers me!
stats.save
end
But i feel there should be something like this:
stats.character << self
Thank You in advance :)
EDIT:
here is how my model look in real life:
def create_stats
race_stats = Race.find(race_id).race_stats
class_stats = RaceClass.find(race_class_id).class_stats
stats = CharacterStats.new
stats.character_id = self.id
stats.health = race_stats.health + class_stats.health
stats.mana = race_stats.mana + class_stats.mana
stats.intellect = race_stats.intellect + class_stats.intellect
stats.armor = race_stats.armor + class_stats.armor
stats.magic_resist = race_stats.magic_resist + class_stats.magic_resist
stats.attack = race_stats.attack + class_stats.attack
stats.defence = race_stats.defence + class_stats.defence
stats.save
self.character_stats_id = stats.id
end
first of all if you want to create CharacterStats after Character is created use after_create callback. About your question you can use ActiveRecord methods which looks like create_character_stats for creating model and build_character_stats for initializing model. So you can change your create_stats method to something like this
def create_stats
self.create_character_stats
end
or you can change your after callback to this
after_create :create_character_stats
and then you don't need any method for this, but in this case you don't have ability to pass attributes to model creating.
also this question mentioned here Using build with a has_one association in rails
in rails way you can use the build_association method refer doc. Also the association should be created only when parent object is created so use after_create callback
class Character < ActiveRecord::Base
has_one :character_stats
after_create :create_stats
def create_stats
state = self.build_character_stats({}) # empty hash as you are not passing any attributes
state.save
end
end
Also, the model name CharacterStats looks plural and that can violate the naming convention and can cause some issue like resolving association class on run time and so.
Related
Here is my issue. I have two models (Construction and Customer)
class Construction < ApplicationRecord
has_many :works
belongs_to :customer
end
class Customer < ApplicationRecord
has_many :constructions
end
I would like to associate a Customer to a Construction during the creation of a new construction.
To do so I have de following controller's method (which is obviously false)
def create
# #construction = Construction.new(constructions_params) (commented)
#construction = Construction.new(customer: #customer)
#customer = Customer.find(params[:customer_id])
#construction.save!
end
from the params I am able to understand that the construction is not saved because it is not attached to a customer and so cannot be created.
I am new to rails and I have been struggling for hours now..
Hope someone will be able to help me.
thanks a lot
Try to revert the order:
#customer = Customer.find(params[:construction][:customer_id])
#construction = Construction.new(customer: #customer)
#construction.save!
you need to assign #customer instance variable before you use it. Otherwise it's nil and nothing is assigned to the new Construction record.
If you have the customer_id available at the point of form creation I reckon that you can do something like this.
Also given the belongs_to relations with the customer on the construction, you should be able to update the customer_id on the construction.
def create
#construction = Construction.new(construction_params)
if #construction.save
# whatever you want to do on success
else
# Whatever you want to do on failure
end
end
# Given you have construction params
private
def construction_params
params.require(:construction).permit(:all, :the, :construction, :attributes, :customer_id)
end
I'm trying to make a STI Base model which changes automatically to inherited class like that:
#models/source/base.rb
class Source::Base < ActiveRecord::Base
after_initialize :detect_type
private
def detect_type
if (/(rss)$/ ~= self.url)
self.type = 'Source::RSS'
end
end
end
#models/source/rss.rb
class Source::RSS < Source::Base
def get_content
puts 'Got content from RSS'
end
end
And i want such behavior:
s = Source::Base.new(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
s2 = Source::Base.first # url is also ending rss
s2.get_content #=> Got content from RSS
There are (at least) three ways to do this:
1. Use a Factory method
#Alejandro Babio's answer is a good example of this pattern. It has very few downsides, but you have to remember to always use the factory method. This can be challenging if third-party code is creating your objects.
2. Override Source::Base.new
Ruby (for all its sins) will let you override new.
class Source::Base < ActiveRecord::Base
def self.new(attributes)
base = super
return base if base.type == base.real_type
base.becomes(base.real_type)
end
def real_type
# type detection logic
end
end
This is "magic", with all of the super cool and super confusing baggage that can bring.
3. Wrap becomes in a conversion method
class Source::Base < ActiveRecord::Base
def become_real_type
return self if self.type == self.real_type
becomes(real_type)
end
def real_type
# type detection logic
end
end
thing = Source::Base.new(params).become_real_type
This is very similar to the factory method, but it lets you do the conversion after object creation, which can be helpful if something else is creating the object.
Another option would be to use a polymorphic association, your classes could look like this:
class Source < ActiveRecord::Base
belongs_to :content, polymorphic: true
end
class RSS < ActiveRecord::Base
has_one :source, as: :content
validates :source, :url, presence: true
end
When creating an instance you'd create the the source, then create and assign a concrete content instance, thus:
s = Source.create
s.content = RSS.create url: exmaple.com
You'd probably want to accepts_nested_attributes_for to keep things simpler.
Your detect_type logic would sit either in a controller, or a service object. It could return the correct class for the content, e.g. return RSS if /(rss)$/ ~= self.url.
With this approach you could ask for Source.all includes: :content, and when you load the content for each Source instance, Rails' polymorphism will instanciate it to the correct type.
If I were you I would add a class method that returns the right instance.
class Source::Base < ActiveRecord::Base
def self.new_by_url(params)
type = if (/(rss)$/ ~= params[:url])
'Source::RSS'
end
raise 'invalid type' unless type
type.constantize.new(params)
end
end
Then you will get the behavior needed:
s = Source::Base.new_by_url(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
And s will be an instance of Source::RSS.
Note: after read your comment about becomes: its code uses klass.new. And new is a class method. After initialize, your object is done and it is a Source::Base, and there are no way to change it.
I need to know if model is in saving state (between before_validate and after_save).
class ModelA < ActiveRecord::Base
before_save: do_before
def do_before
modelb.create(:attr => 123, :ref => self)
end
end
class ModelB < ActiveRecord::Base
before_create: do_before
def do_before
self.ref.my_attr = 321
self.ref.save! unless self.ref.is_saving?
end
end
I need "is_saving?" attribute for every model instance in my project. What is the best way to implement that?
Can't you rather set an instance variable in between :before_save and :after_save by using ActiveRecord's :around_save callback and then yield your save? Anyway, the question is not too clear to me. What is the purpose of is_saving? method?
In Rails/ActiveReocrd is there a way to replace one instance with another such that all the relations/foreign keys get resolved.
I could imagine something like this:
//setup
customer1 = Customer.find(1)
customer2 = Customer.find(2)
//this would be cool
customer1.replace_with(customer2)
supposing customer1 was badly configured and someone had gone and created customer2, not knowing about customer1 it would be nice to be able to quickly set everything to customer 2
So, also this would need to update any foreign keys as well
User belongs_to :customer
Website belongs_to :customer
then any Users/Websites with a foreign key customer_id = 1 would automatically get set to 2 by this 'replace_with' method
Does such a thing exist?
[I can imagine a hack involving Customer.reflect_on_all_associations(:has_many) etc]
Cheers,
J
Something like this could work, although there may be a more proper way:
Updated: Corrected a few errors in the associations example.
class MyModel < ActiveRecord::Base
...
# if needed, force logout / expire session in controller beforehand.
def replace_with (another_record)
# handles attributes and belongs_to associations
attribute_hash = another_record.attributes
attribute_hash.delete('id')
self.update_attributes!(attribute_hash)
### Begin association example, not complete.
# generic way of finding model constants
find_model_proc = Proc.new{ |x| x.to_s.singularize.camelize.constantize }
model_constant = find_model_proc.call(self.class.name)
# handle :has_one, :has_many associations
have_ones = model_constant.reflect_on_all_associations(:has_one).find_all{|i| !i.options.include?(:through)}
have_manys = model_constant.reflect_on_all_associations(:has_many).find_all{|i| !i.options.include?(:through)}
update_assoc_proc = Proc.new do |assoc, associated_record, id|
primary_key = assoc.primary_key_name.to_sym
attribs = associated_record.attributes
attribs[primary_key] = self.id
associated_record.update_attributes!(attribs)
end
have_ones.each do |assoc|
associated_record = self.send(assoc.name)
unless associated_record.nil?
update_assoc_proc.call(assoc, associated_record, self.id)
end
end
have_manys.each do |assoc|
associated_records = self.send(assoc.name)
associated_records.each do |associated_record|
update_assoc_proc.call(assoc, associated_record, self.id)
end
end
### End association example, not complete.
# and if desired..
# do not call :destroy if you have any associations set with :dependents => :destroy
another_record.destroy
end
...
end
I've included an example for how you could handle some associations, but overall this can become tricky.
Clients have many Invoices. Invoices have a number attribute that I want to initialize by incrementing the client's previous invoice number.
For example:
#client = Client.find(1)
#client.last_invoice_number
> 14
#invoice = #client.invoices.build
#invoice.number
> 15
I want to get this functionality into my Invoice model, but I'm not sure how to. Here's what I'm imagining the code to be like:
class Invoice < ActiveRecord::Base
...
def initialize(attributes = {})
client = Client.find(attributes[:client_id])
attributes[:number] = client.last_invoice_number + 1
client.update_attributes(:last_invoice_number => client.last_invoice_number + 1)
end
end
However, attributes[:client_id] isn't set when I call #client.invoices.build.
How and when is the invoice's client_id initialized, and when can I use it to initialize the invoice's number? Can I get this logic into the model, or will I have to put it in the controller?
Generate a migration that adds invoices_number column to users table. Then in Invoice model write this:
class Invoice < ActiveRecord::Base
belongs_to :user, :counter_cache => true
...
end
This will automatically increase invoices_count attribute for user once the invoice is created.
how about this:
class Invoice < ActiveRecord::Base
...
def initialize(attributes = {})
super
self.number = self.client.invoices.size + 1 unless self.client.nil?
end
end
Here is some useful discussion on after_initialize per Jonathan R. Wallace's comment above:
http://blog.dalethatcher.com/2008/03/rails-dont-override-initialize-on.html
first of all, you don't need to use the attributes collection, you can just do self.client_id. Better yet, as long as you have a belongs_to :client in your Invoice, you could just do self.client.last_invoice_number. Lastly, you almost always want to raise an exception if an update or create fails, so get used to using update_attributes!, which is a better default choice. (if you have any questions about those points, ask, and I'll go into more detail)
Now that that is out of the way, you ran into a bit of a gotcha with ActiveRecord, initializer methods are almost never the right choice. AR gives you a bunch of methods to hook into whatever point of the lifecycle you need to. These are
after_create
after_destroy
after_save
after_update
after_validation
after_validation_on_create
after_validation_on_update
before_create
before_destroy
before_save
before_update
before_validation
before_validation_on_create
before_validation_on_update
What you probably want is to hook into before_create. Something like this
def before_create
self.number ||= self.client.last_invoice_number + 1 unless self.client
end
What that will do is it will hit up the database for your client, get the last invoice number, increment it by one, and set it as its new number, but only if you haven't already set a number (||= will assign, but only if the left side is nil), and only if you have set a client (or client_id) before the save.