Considering the following
:company has_many :issues #ticket tracking example
I'd like to have routing such that any user in the company can go to /issues/:id (which is simple enough when using the default id column).
However, i'd like instead to have an issue id specific to the company (so each company would have their own issue 1, 2, 3 etc that isn't unique (and wouldn't use the internal id).
Is there any better way than calculating an id based on the last number in the db in the create and update actions in IssueController for that company id? (I can think of various issues here with race conditions when multiple records are being updated/created per company).
Thanks in advance!
I would take the issue_id handling down to the model level. You can use a before_validation callback to set the issue_id. This alone, even though the save call is wrapped in a transaction, won't prevent race conditions. You have to further ensure the uniqueness of the couple [ :company_id, :issue_id ] by adding a index/unique constraint to your issues table, as suggested by #PinnyM. So something like this
class Issue < ActiveRecord::Base
attr_accessible :company_id, :issue_id
belongs_to :company
before_validation :set_issue_id
private
def set_issue_id
self.issue_id = self.company.issues.size + 1
end
end
class Company < ActiveRecord::Base
has_many :issues
end
and inside a migration:
add_index :issues, [:issue_id, :company_id], :unique => true
And you could grab the correct issue in the controller like you said:
#issue = Issue.where(company_id: current_user.company.id, issue_id: params[:id])
Note that this doesn't provide a way to recover from a constraint violation exception in case that actually happened. See #PinnyM answer for a suggestion on how to handle this.
Hope this helps.
After misunderstanding the question the first time around, I'll take another shot at it.
You are looking for a way to have an issue 'counter' (to simulate an id) that is specific to a company_id. Although #deivid was on the right track, relying on issues.count will only work if you can guarantee that a company never makes more than one request at a time. This will generally be the case, but is not guaranteed and shouldn't be used alone as the core logic behind this counter.
You'll need to add a unique constraint/index to your issues table - this will ensure that counter can't be duplicated:
add_index :issues, [:issue_id, :company_id], :unique => true
Adding a :uniqueness constraint in the model will only mitigate the problem somewhat by making the window for the race condition smaller, but it can't guarantee uniqueness completely.
Note that in the event of a constraint violation in the DB, ActiveRecord can't recover from it within the transaction. You can try rescuing ActiveRecord::RecordNotUnique, recreating the issue (or just regenerating the issue_id using a fresh count) and saving again.
Related
I'm having a hard time trying to avoid duplicates in my database and really curious to know if I'm actually doing this correctly. In the example below, my technical reports actually work perfectly fine. So there's no duplicate nodes in the technical report, even if I try to create a duplicate manually.
However, I can't say the same for the nodes' vulns. When I import the exact same file (containing data) twice, I get duplicate Vulns for the same IP when I believe it's not supposed to happen.
So here are two models that I have:
#app/models/node.rb
class Node < ActiveRecord::Base
validates_uniqueness_of :technical_report_id, :scope => :ip
has_many :vulns, dependent: :destroy
end
.
#app/models/vuln.rb
class Vuln < ActiveRecord::Base
validates_uniqueness_of :node_id, :scope => [:master_finding_id, :vuln_finding_id, :additional_output, :port]
belongs_to :node
belongs_to :master_finding
belongs_to :vuln_finding
end
However, when I go to importing data, I still find myself with duplicates in the Vuln table. I've used rails c to validate this as well.
irb(main):012:0> Vuln.where(node_id: 12).pluck(:master_finding_id, :additional_output, :vuln_finding_id).length
(0.4ms) SELECT `vulns`.`master_finding_id`, `vulns`.`additional_output`, `vulns`.`vuln_finding_id` FROM `vulns` WHERE `vulns`.`node_id` = 12
=> 2
When I go to call .uniq it shows that there's only one entry.
irb(main):013:0> Vuln.where(node_id: 12).pluck(:master_finding_id, :additional_output, :vuln_finding_id).uniq.length
(0.5ms) SELECT `vulns`.`master_finding_id`, `vulns`.`additional_output`, `vulns`.`vuln_finding_id` FROM `vulns` WHERE `vulns`.`node_id` = 12
=> 1
Does anyone know what I'm doing wrong here? I'm not sure why this works for one model and not the other one. If I try to create two of the exact same Vuln records from rails c CLI, it rolls back like it should have, but not when it's created otherwise.
EDIT
Looks like my issue is with activerecord-import instead. I'm not sure why but it imports nodes just fine, but it calls the "Class Create Many Without Validations Or Callbacks" when it goes to importing vulns. Guess because there's a lot more data or something. I think I have this sort of figured out for now. Perhaps I just need to write a quick method to validate the uniqueness manually since I don't want to get rid of the mass importing gem.
validates_uniqueness_of can not ensure uniqueness in every case. Within the docs you can see an example for the race condition within this kind of validation. If it is realy needed you should also use an uniq constraint on database level.
I have few question that bugs me off and need to be answered. Everything is related to the following tutorial Two Many-to-Many
Question 1
Does the join table using has_many need to have an id? or its best practice to remove the id? and add an index and using the two other primary key and set it unique and together?
Question 2
How can it be done in the migration of creating a table?
Question 3
After doing these relationship model and updating the data. I would like to create a new set of data everytime it is updated (to preserve the data). How would a controller would look in the update, new, create model?
Question 4
In the the middle table, I would like to set attributes such has a visible true, or false, how can I set also not just the third table but also the second table arguments
First ... a word of caution: That railscast is very old. There may be syntactical things in that episode that have been dated by new versions of rails.
Question 1
If you are using the has_many through method then you have to have an id column in the join model because you are using a full blown model. As Ryan mentions in the episode, you'll choose this method if you need to track additional information. If you use the has_and_belongs_to_many method, you will not have an id column in your table.
If you want to achieve a check where you do not allow duplicates in your many-to-many association (ie allow the pairing of item a with item b and again allowing another record of item a to item b), you can use a simple validates line with a scope:
validates_uniqueness_of :model_a_id, :scope => [:model_b_id]
Question 2
You can add indices in your migrations with this code
add_index :table_name, [ :join_a_id, :join_b_id ], :unique => true, :name => 'by_a_and_b'
This would be inserted into the change block below your create_table statement (but not in that create_table block). Check out this question for some more details: In a join table, what's the best workaround for Rails' absence of a composite key?
Question 3
I'm not completely clear on what you're looking to accomplish but if you want to take some action every time a new record is inserted into the join model I would use the after_create active record hook. That would look something like this.
class YourJoinModel < ActiveRecord::Base
after_create :do_something
def do_something
puts "hello world"
end
end
That function, do_something, will be called each time a new record is created.
Question 4
Using the has_many through method will give you access to the additional attributes that you defined in that model on both sides of the relationship. For example, if you have this setup:
class Factory < ActiveRecord::Base
has_many :widgets, :through => :showcases
end
class Widget < ActiveRecord::Base
has_many :factories, :through => :showcases
end
class Showcases < ActiveRecord::Base
belongs_to :factory
belongs_to :widget
attr_accessiable :factory_id, :widget_id, :visible
end
You could say something like
widget = Widget.first
shown = widget.showcases
shown.first.visible
or
shown = widget.showcases.where( :visible=> true )
You can also reach to the other association:
shown.first.factory
The reason for having an id column in an association is it gives you a way of deleting that specific association without concerning yourself with the relationship it has. Without that identifier, associations are hard to define outside of specifying all foreign keys.
For a trivial case where you have only two components to your key, this isn't that big a differentiator, but often you will have three or more as part of your unique constraint and there's where things get tricky.
Having an id also makes the relationship a first-class model. This can be useful when you're manipulating elements that have associated meta-data. It also means you can add meta-data effortlessly at a later date. This is what you mean by your "Question 4". Add those attributes to the join model.
Generally the join model is created like you would any other model. The primary key is the id and you create a series of secondary keys:
create_table :example_things |t|
t.integer :example_id
t.integer :thing_id
end
add_index :example_joins, [ :example_id, :thing_id ], :unique => true
add_index :example_joins, :thing_id
The main unique index serves to prevent duplication and allows lookups of key-pairs. The secondary serves as a way of extracting all example_id for a given thing_id.
The usual way to manipulate meta-data on the join model is to fetch those directly:
#example_things = #example.example_things.includes(:thing)
This loads both the ExampleThing and Thing models associated with an Example.
One of the models in a Rails 3.1 application I'm working on has a "code" attribute that is generated automatically when the record is created and that must be unique. The application should check the database to see if the generated code exists and, if it does, it should generate a new code and repeat the process.
I can ensure the field's uniqueness at the database level with add_index :credits, :code, :unique => true (which I am doing) and also in the model with validates_uniqueness_of, but both of these will simply return an error if the generated code exists. I need to just try again in the case of a duplicate. The generated codes are sufficiently long that duplicates are unlikely but I need to be 100% certain.
This code generation is handled transparently to the end user and so they should never see an error. Once the code is generated, what's the best way to check if it exists and to repeat the process until a unique value is found?
Here's a quick example, there is still technically a race condition here, though unless your seeing hundreds or thousands of creates per second it really shouldnt be a worry, worst case is your user gets a uniquness error if two creates are run in such a way that they both execute the find and return nil with the same Url
class Credit < ActiveRecord::Base
before_validation :create_code, :if => 'self.new_record?'
validates :code, :uniqueness => true
def create_code
self.code = code_generator
self.code = code_generator until Credit.find_by_code(code).nil?
end
end
If you absolutely needed to remove the race condition case where two creates are running in tandem and both trigger the find with the same code and return nil you could wrap the find with a table lock which requires DB specific SQL, or you could create a table that has a row used for locking on via pessimistic locking, but I wouldn't go that far unless your expecting hundreds of creates per second and you absolutely require that the user never ever sees an error, it's doable, just kind of overkill in most cases.
I am not sure if there is a built in way. I have always used a before_create.
Here is an example in the context of a UrlShortener.
class UrlShortener < Activerecord::Base
before_create :create_short_url
def create_short_url
self.short_url = RandomString.generate(6)
until UrlShortener.find_by_short_url(self.short_url).nil?
self.short_url = RandomString.generate(6)
end
end
end
I have a Rails application, with two models: SalesTransactions and PurchaseOrders.
In the PurchaseOrders model, new entries are registered using 'purchase_order_number' as the key field. I use the create method of the model to search if that 'purchase_order_number' has been previously registered, and if so, reuse that record and use its id in the SalesTransaction record. If that name wasn't already registered, I go ahead and perform the create, and then use the new PurchaseOrder record id in the SalesTransaction (the foreign_id linking to the associated PO).
Note that I don't have the existing PurchaseOrder record id until I've done a look-up in the create method (so this is not a question of 'how do I update a record using 'accepts_nested_attributes_for'?', I can do that once I have the id).
In some situations, my application records a new SalesTransaction, and creates a new PurchaseOrder at the same time. It uses accepts_nested_attributes_for to create the PurchaseOrder record.
The problem appears to be that when using 'accepts_nested_attributes_for', create is not called and so my model does not have the opportunity to intercept the create, and look-up if the 'purchase_order_number' has already been registered and handle that case.
I'd appreciate suggestions as to how to intercept 'accepts_nested_attributes_for' creations to allow some pre-processing (i.e. look up if the PurchaseOrder record with that number already exists, and if so, use it).
Not all Sales have a PurchaseOrder, so the PurchaseOrder record is optional within a SalesTransaction.
(I've seen a kludge involving :reject_if, but that does not allow me to add the existing record id as the foreign_id within the parent record.)
Thanks.
You could use validate and save callbacks to do what you need.
Assuming the setup:
class SalesTransaction < ActiveRecord::Base
belongs_to :purchase_order, :foreign_key => "po_purchase_order_no",
:primary_key => "purchase_order_no"
accepts_nested_attributes_for :purchase_order
end
class PurchaseOrder < ActiveRecord::Base
has_many :sales_transactions, :foreign_key => "po_purchase_order_no",
:primary_key => "purchase_order_no"
before_validation :check_for_exisitng_po # maybe only on create?
accepts_nested_attributes_for :sales_transactions
private
def check_for_exisitng_po
existing_po = PurchaseOrder.find_by_purchase_order_no(self.purchase_order_no)
if existing_po
self.id = existing_po.id
self.reload # don't like this, also will overwrite incoming attrs
#new_record = false # tell AR this is not a new record
end
true
end
end
This should give back full use of accepts_nested_attributes_for again.
gist w/tests
Two ideas: Have you taken a look at association callbacks? Perhaps you can "intercept" accepts_nested_attributes_for at this level, using :before_add to check if it is already in the DB before creating a new record.
The other idea is to post-process instead. In an after_save/update you could look up all of the records with the name (that ought to be unique), and if there's more than one then merge them.
I was going to write a before_save function, but you say this:
It uses accepts_nested_attributes_for to create the PurchaseOrder record.
So in the SalesTransaction process flow, why look it up at all? You should just get the next one available... there shouldn't be a reason to search for something that didn't exist until NOW.
OK, I've left this question out there for a while, and offered a bounty, but I've not got the answer I'm looking for (though I certainly appreciate folk trying to help).
I'm concluding that I wasn't missing some trick and, at the time of writing, there isn't a neat solution, only work-arounds.
As such, I'm going to rewrite my App to avoid using accept_nested_attributes_for, and post the SalesTransaction and the PurchaseOrder records separately, so the create code can be applied in both cases.
A shame, as accept_nested... is pretty cool otherwise, but it's not complete enough in this case.
I still love Rails ;-)
So I have some models set up that can each have a comment. I have it set up using has_many_polymorphs, but I'm starting to run into some issues where it's not working how I think it should.
For example:
class Project < ActiveRecord::Base
end
class Message < ActiveRecord::Base
has_many_polymorphs :consumers,
:from => [:projects, :messages],
:through => :message_consumers,
:as => :comment # Self-referential associations have to rename the non-polymorphic key
end
class MessageConsumer < ActiveRecord::Base
# Self-referential associations have to rename the non-polymorphic key
belongs_to :comment, :foreign_key => 'comment_id', :class_name => 'Message'
belongs_to :consumer, :polymorphic => true
end
In this case, the Message wouldn't get deleted when the Project is removed, because the Message is really the parent in the relationship.
I simplified it a little for the example, but there are other models that have have a Message, and there are also Attachments that work similarly.
What would be the correct way to set this up so that the children get removed when the parent is deleted? I'm hoping to not have a million tables, but I can't quite figure out another way to do this.
When you say "so that the children get removed when the parent is deleted?", can you give an example? I.e. when a project is deleted I want all its messages to be deleted too? What happens when you delete a message, do you want anything else (e.g. all corresponding message_consumer entries) to be deleted as well?
UPDATE
OK, so has_many_polymorphs will automatically delete "orphaned" message_consumers. Your problem is that a message may have more than one consumer, so deleting a project may not be sufficient grounds for deleting all its associated messages (as other consumers may depend on those messages.)
In this particular case you can set up an after_destroy callback in MessageConsumer, to check whether there still exist other MessageConsumer mappings (other than self) that reference the Message and, if none exist, also delete the message, e.g.:
class MessageConsumer < ActiveRecord::Base
...
after_destroy :delete_orphaned_messages
def delete_orphaned_messages
if MessageConsumer.find(:first, :conditions => [ 'comment_id = ?', self.comment_id] ).empty?
self.comment.delete
end
end
end
All this is happening inside a transaction, so either all deletes succeed or none succeed.
You should only be aware of potential race conditions whereby one session would arrive at the conclusion that a Message is no longer used, whereas another one may be in the process of creating a new MessageConsumer for that exact same Message. This can be enforced by referential integrity at the DB level (add foreign key constraints, which will make on of the two sessions fail, and will keep your database in a consistent state), or locking (ugh!)
You could simplify this a lot by using acts_as_commentable.