I'm pretty new to ruby and ruby-on-rails, so I need to learn best practices.
I have Tag model, each tag can have many sub-tags and many super-tags:
has_many :super_tags, :through => :tag_hier_rels, :source => :super_tag
has_many :sub_tags, :through => :reverse_tag_hier_rels, :source => :sub_tag
has_many :tag_hier_rels, :foreign_key => "sub_tag_id"
has_many :reverse_tag_hier_rels, :foreign_key => "super_tag_id", :class_name => "TagHierRel"
I need to prevent user to create circular references. But with auto-generated methods :super_tags and :sub_tags I can't do this: everyone can do something like:
tag.super_tags.push another_tag, and I have no control on this.
Rails validation mechanism isn't useful here: this mechanism prevents user to save invalid objects to database, but I need to prevent him even to modify object incorrectly: if I have circular reference, and I need to get all the sub-tags or super-tags recursively, I will run into stack overflow.
So I have done the following:
Declared these associations as private ones:
private :sub_tags=, :sub_tags
private :super_tags=, :super_tags
Added methods with _copy postfix:
def sub_tags_copy
return sub_tags.clone
end
def super_tags_copy
return super_tags.clone
end
And added methods that actualy modify arrays:
def sub_tags_push(tag)
sub_tags.push tag if !self.all_sub_tags.include? tag and !self.all_super_tags.include? tag
end
def super_tags_push(tag)
super_tags.push tag if !self.all_sub_tags.include? tag and !self.all_super_tags.include? tag
end
# TODO: more methods (at least we need to remove tags)
(methods all_sub_tags and all_super_tags generate arrays recursively)
It works, but I don't really like this solution: at least, it's not obvious for user that he should use ..._copy methods.
Probably I am doing this wrong?
UPD:
Or, is it bad practice in general to disallow user to change an object in wrong way? Probably I should allow user to change an object in wrong way, but only validate it before saving?
At least, I already figured out that it's hard to supply error messages if user does something wrong: currently, model just silently does not modify an object, and no error message is generated. I have to implement my own error messages engine, and this fact is an evidence that approach is really bad... I seem to struggle against the framework instead of using it.
You are always referring to user where you mean programmers?
If that is the case, you should try to assert this kind of stuff through
Education (developer to developer)
Validation (via tests or rails validations)
Constraints (on the database level through db constraints or triggers)
I would try to do it top down.
Related
I am having a very strange problem in Rails. I am using update_attributes on a Parent Object to update all the children objects. The children objects have (custom) validation and indeed this works ok, meaning that if I give wrong values the validation trigger and I get an error back.
Now I am in a strange situation where one of the model is invalid in the database (let's not question why, let's just say I can go in the DB and run some SQL to make the model invalid). If I go in my app I can see the invalid values and this is fine. I fix the values and save again and I can see, stepping in the ruby code that the validation is called also BEFORE saving the new values, meaning that I will get an error and Rails will never execute the SQL to actually update the values to the correct ones.
I hope the above makes sense. Do you have any idea or do you think there is something I am overlooking?
SOLUTION:
What was happening was that a many-to-many relationship was validating the existing DB data before being replaced by the new data. Basically the structure was like this:
class User
has_many :user_permissions
has_many :permissions, :through => :model_permissions
class Permission
has_many :user_permissions
has_many :users, :through => :user_permissions
class UserPermission
belongs_to :user
belongs_to :permission
validates_associated :user # THIS was causing the problem
validates_associated :permission # and THIS as well
I simply removed the validates_associated directive, since I am validating the linked records independently anyway.
Well, Rails does run your validation prior to writing the data to the database (please refer to Active Record callback sequence), so having validation errors means that some piece of the model you are trying to save is not valid. It might an associated model containing errors with the validation turned on or just some missing part – in any case, just have a look at what are the errors you are getting.
In case (let's no question why either :) you want to skip validation - you are open to choose from #update_attribute (to update just one attribute), calling #save(false), using +udpate_all method of the model class or even go down to ActiveRecord::Base.connection.execute – none of these will ever bother you with validation errors :)
I have a Game model which has_many :texts. The problem is that I have to order the texts differently depending on which game they belong to (yes, ugly, but it's legacy data). I created a Text.in_game_order_query(game) method, which returns the appropriate ordering.
My favourite solution would have been to place a default scope in the Text model, but that would require knowing which game they're part of. I also don't want to create separate classes for the texts for each game - there are many games, with more coming up, and all the newer ones will use the same ordering. So I had another idea: ordering texts in the has_many, when I do know which game they're part of:
has_many :texts, :order => Text.in_game_order_query(self)
However, self is the class here, so that doesn't work.
Is there really no other solution except calling #game.texts.in_game_order(#game) every single time??
I had a very similar problem recently and I was convinced that it wasn't possible in Rails but that I learned something very interesting.
You can declare a parameter for a scope and then not pass it in and it will pass in the parent object by default!
So, you can just do:
class Game < ActiveRecord
has_many :texts, -> (game) { Text.in_game_order_query(game) }
Believe or not, you don't have to pass in the game. Rails will do it magically for you. You can simply do:
game.texts
There is one caveat, though. This will not work presently in Rails if you have preloading enabled. If you do, you may get this warning:
DEPRECATION WARNING: The association scope 'texts' is instance dependent (the scope block takes an argument). Preloading happens before the individual instances are created. This means that there is no instance being passed to the association scope. This will most likely result in broken or incorrect behavior. Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
Following up using PradeepKumar's idea, I found the following solution to work
Assuming a class Block which has an attribute block_type, and a container class (say Page), you could have something like this:
class Page
...
has_many :blocks do
def ordered_by_type
# self is the array of blocks
self.sort_by(&:block_type)
end
end
...
end
Then when you call
page.blocks.ordered_by_type
you get what you want - defined by a Proc.
Obviously, the Proc could be much more complex and is not working in the SQL call but after there result set has been compiled.
UPDATE:
I re-read this post and my answer after a bunch of time, and I wonder if you could do something as simple as another method which you basically suggested yourself in the post.
What if you added a method to Game called ordered_texts
def ordered_texts
texts.in_game_order(self)
end
Does that solve the issue? Or does this method need to be chainable with other Game relation methods?
Would an Association extension be a possibility?
It seems that you could make this work:
module Legacy
def legacy_game_order
order(proxy_association.owner.custom_texts_order)
end
end
class Game << ActiveRecord::Base
includes Legacy
has_many :texts, :extend => Legacy
def custom_texts_order
# your custom query logic goes here
end
end
That way, given a game instance, you should be able to access instance's custom query without having to pass in self:
g = Game.find(123)
g.texts.legacy_game_order
Here is a way where you can do it,
has_many :texts, :order => lambda { Text.in_game_order_query(self) }
This is another way which I usually wont recommend(but will work),
has_many :texts do
def game_order(game)
find(:all, :order => Text.in_game_order_query(game))
end
end
and you can call them by,
game.texts.game_order(game)
Im not sure what your order/query looks like in the in_game_order_query class method but i believe you can do this
has_many :texts, :finder_sql => proc{Text.in_game_order_query(self)}
Just letting you know that I have never used this before but I would appreciate it if you let me know if this works for you or not.
Check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many for more documentation on :finder_sql
I think if you want runtime information processed you should get this done with:
has_many :texts, :order => proc{ {Text.in_game_order_query(self)} }
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 ;-)
I'm creating a tag system right now where Post has_many :tags, :through => :tag_joins
Right now when a new tag is created, a join is automatically created, connecting the tag and post the tag was created on. The problem is I'm trying to use before_create to check if a tag with the same name and user ID has already been created. If it has already been created, I'd like the join to use the original tag ID, instead of letting it create a new tag ID.
Any tips for how I can accomplish this?
Why don't you use find_or_create_by_user_id_and_tag_id() or find_or_initialize_by_.
Update
So if you want to avoid creating duplicate tags, you can just use:
#post.tags.find_or_create_by_name('tag_name')
or if you want to apply some changes before saving the new object then use
#post.tags.find_or_initialize_by_name('tag_name')
In both cases the name attribute will be set to 'tag_name' by default.
So this method will return you the tag if exists, otherwise creates it, so you can use this when you set up your join model.
Update 2
This is actually not gonna work with has_many :through, you can see a similar problem and a workaround here:
Error while using `find_or_create_by` on a `has_many` `through` association
Can't you run a private method in your model using :before_save?
If you put code like:
:before_save :method_to_check_something
...you will be able to run any manner of validation in the model without getting the controller involved (and thereby adhering to the skinny controller, fat model methodology).
This should take care of duplicate records between the Post and the Tag but not sure how your associations are set up with the User.
class Post < ActiveRecord::Base
has_many :tags, :through => :tag_joins, :uniq => true
end
The business logic is this: Users are in a Boat through a join table, I guess let's call that model a Ticket. But when a User instance wants to check who else is on the boat, there's a condition that asks if that user has permission see everyone on the Boat, or just certain people on the Boat. If a User can see everyone, the normal deal is fine: some_user.boats.first.users returns all users with a ticket for that boat. But for some users, the only people that are on the boat (as far as they're concerned) are people in, let's say the dining room. So if User's ticket is "tagged" (using an acts_as_taggable style system) with "Dining Room", the only Users returned from some_user.boats.first.users should be Users with tickets tagged "Dining Room".
Just for the record, I'm not trying to design something to be insane from the getgo - I'm trying to wedge this arbitrary grouping into a (mostly) existent system.
So we've got:
class User
has_many :tickets
has_many :boats, :through => :tickets
end
class Ticket
belongs_to :user
belongs_to :boat
end
class Boat
has_many :tickets
has_many :users, :through => :tickets
end
Initially, I thought that I could conditionally modify the virtual class like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
has_many :tickets, :include => :tags, :conditions => ['tags.id in (?)', [#{tag_ids.to_s(:db)}]]
code
)
That gets all the way down to generating the SQL, but when generated, it generates SQL ending in:
LEFT OUTER JOIN "tags" ON ("tags"."id" = "taggings"."tag_id") WHERE ("tickets"._id = 1069416589 AND (tags.id in (5001,4502)))
I've tried digging around the ActiveRecord code, but I can't find anywhere that would prefix that 'id' in the SQL above with an underscore. I know that associations are loaded when an ActiveRecord class is loaded, and I'd assume the same with a singleton class. shrug.
I also used an alias_method_chain like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
def tickets_with_tag_filtering
tags = Tag.find(etc, etc)
tickets_without_tag_filtering.scoped(:include => :tags, :conditions => {:'tags.id' => tags})
end
alias_method_chain :tickets, :tag_filtering
code
)
But while that approach produces the desired Tickets, any joins on those tickets use the conditions in the class, not the virtual class. some_user.boats.first.users returns all users.
Any type of comment will be appreciated, especially if I'm barking up the wrong tree with this approach. Thanks!
So a wild guess about your underscore issue is that Rails is generating the assocation code based on the context at the time of evaluation. Being in a singleton class could mess this up, like so:
"#{owner.table_name}.#{association.class.name}_id = #{association.id}"
You could get in there and define a class name property on your singleton class and see if that fixes the issue.
On the whole I don't recommend this. It creates behavior that is agonizing to track down and impossible to extend effectively. It creates a landmine in the codebase that will wound you or someone you love at a later time.
Instead, consider using a named_scope declaration:
class User
has_many :taggings, :through => :tickets
named_scope :visible_to, lambda { |looking_user|
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
}
end
While you may have to go back and edit some code, this is much more flexible in the ways it can be used:
Boat.last.users.visible_to( current_user )
It's clear that a restriction is being placed on the find, and what the purpose of that restriction is. Because the conditions are dynamically calculated at runtime, you can deal with the next weird modification your client hits you with. Say some of their users have xray vision and clairvoyance:
class User
named_scope :visible_to, lambda { |looking_user|
if looking_user.superhuman?
{}
else
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
end
}
end
By returning an empty hash, you can effectively nullify the effect of the scope.
Why not just grab all users on the boat and include their tags.
Then run a quick filter to include & return only the users with the same tag as the inquiring user.
What version of Rails are you using? Have you tried upgrading to see if the underscore issue is fixed? It's like it can't find the foreign key to put in as "tag_id" or somethin'.
My ruby-fu is limited, so I'm not sure how to dynamically include the correct method options at run-time.
Just to help you clarify, you have to worry about this two places. You want to filter a user's viewable users so they only see users with the same tags. Your structure is:
user <--> tickets <--> boats <--> tickets <--> users
... right?
So, you need to filter both sets of tickets down to the ones with the current_user's tags.
Maybe you just need a current_user.viewable_users() method and then filter everything through that? I'm not sure what existing functionality you've got to preserve.
Blech, I don't feel like I'm helping you at all. Sorry.
Your approach is the problem. I know it seems expedient at the moment to hack something in where you don't have to refactor the existing call sites, but I believe given time this will come back to haunt you as the source of bugs and complexity.
Sleeping dogs that lie come back to bite you hard, in my experience. Typically in the form of a future developer who doesn't know your association is "magic" and uses it assuming it's just pail ole rails. He/she likely won't even have a reason to write a test case that would expose the behavior either, which raises the odds you'll only find out about the bug when it's live in production and the client is unhappy. Is it really worth the time you're saving now?
Austinfrombostin is pointing the way. Different semantics? Different names. Rule number one is always to write code that says what it does as clearly as possible. Anything else is the path of madness.