How can I stop collection<<(object, …) from adding duplicates? - ruby-on-rails

Given a Dinner model that has many Vegetable models, I would prefer that
dinner.vegetables << carrot
not add the carrot if
dinner.vegetables.exists? carrot
Yet it does. It will add a duplicate record every time << is called.
There is a :uniq option you can set on the association, but it only FETCHES AND RETURNS one result if there are multiples, it doesn't ENFORCE unique values.
I could check for exists? every time I add an obj to a collection, but that is tedious and error-prone.
How can I use << freely and not worry about errors and not check for already existing collection members every time?

The best way is to use Set instead of Array:
set = Set.new
set << "a"
set << "a"
set.count -> returns 1

You can add an ActiveRecord unique constraint if you have a join model representing a many-to-many relationship between dinners and vegetables. That's one reason I use join models and has_many :through as opposed to has_and_belongs_to_many. It's important to add a uniqueness constraint at the database level if possible.
UPDATE:
To use a join model to enforce constraint you would need an additional table in your database.
class Dinner
has_many :dinner_vegetables
has_many :vegetables, :through => :dinner_vegetables
end
class Vegetable
has_many :dinner_vegetables
has_many :dinners, :through => :dinner_vegetables
end
class DinnerVegetable
belongs_to :dinner
belongs_to :vegetable
validates :dinner_id, :uniqueness => {:scope => :vegetable_id} # You should also set up a matching DB constraint
end

The other posters' ideas are fine, but as another option you can also enforce this on the database level using e.g. the UNIQUE constraint in MySQL.

After a lot of digging, I've discovered something cool: before_add, which is an association callback, which I never knew even existed. So I could do something like this:
has_many :vegetables, :before_add => :enforce_unique
def enforce_unique(assoc)
if exists? assoc
...
end
Doing this at the DB level is a great idea if you REALLY NEED this to be unique, but in the case that it's not mission critical the solution above is enough for me.
It's mostly to avoid the icky feeling of having extra records lying around in the db...

Related

Mongoid: How do I query for all object where the number of has_many object are > 0

I have a Gift model:
class Gift
include Mongoid::Document
include Mongoid::Timestamps
has_many :gift_units, :inverse_of => :gift
end
And I have a GiftUnit model:
class GiftUnit
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :gift, :inverse_of => :gift_units
end
Some of my gifts have gift_units, but others have not. How do I query for all the gifts where gift.gift_units.size > 0?
Fyi: Gift.where(:gift_units.exists => true) does not return anything.
That has_many is an assertion about the structure of GiftUnit, not the structure of Gift. When you say something like this:
class A
has_many :bs
end
you are saying that instance of B have an a_id field whose values are ids for A instances, i.e. for any b which is an instance of B, you can say A.find(b.a_id) and get an instance of A back.
MongoDB doesn't support JOINs so anything in a Gift.where has to be a Gift field. But your Gifts have no gift_units field so Gift.where(:gift_units.exists => true) will never give you anything.
You could probably use aggregation through GiftUnit to find what you're looking for but a counter cache on your belongs_to relation should work better. If you had this:
belongs_to :gift, :inverse_of => :gift_units, :counter_cache => true
then you would get a gift_units_count field in your Gifts and you could:
Gift.where(:gift_units_count.gt => 0)
to find what you're looking for. You might have to add the gift_units_count field to Gift yourself, I'm finding conflicting information about this but I'm told (by a reliable source) in the comments that Mongoid4 creates the field itself.
If you're adding the counter cache to existing documents then you'll have to use update_counters to initialize them before you can query on them.
I tried to find a solution for this problem several times already and always gave up. I just got an idea how this can be easily mimicked. It might not be a very scalable way, but it works for limited object counts. The key to this is a sentence from this documentation where it says:
Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.
So, get this done, you can define a class function like so:
def self.with_units
ids = Gift.all.select{|g| g.gift_units.count > 0}.map(&:id)
Gift.where(:id.in => ids)
end
The advantage is, that you can do all kinds of queries on the associated (GiftUnits) model and return those Gift instances, where those queries are satisfied (which was the case for me) and most importantly you can chain further queries like so:
Gift.with_units.where(:some_field => some_value)

How to save related Models in one transaction?

I have two models:
class Customer < ActiveRecord::Base
has_many :contacts
end
class Contact < ActiveRecord::Base
belongs_to :customer
validates :customer, presence: true
end
Then, in my controller, I would expect to be able to create both in
"one" sweep:
#customer = Customer.new
#customer.contacts.build
#customer.save
This, fails (unfortunately translations are on, It translates to
something like: Contact: customer cannot be blank.)
#customer.errors.messages #=> :contacts=>["translation missing: en.activerecord.errors.models.customer.attributes.contacts.invalid"]}
When inspecting the models, indeed, #customer.contacts.first.customer
is nil. Which, somehow, makes sense, since the #customer has not
been saved, and thus has no id.
How can I build such associated models, then save/create them, so that:
No models are persisted if one is invalid,
the errors can be read out in one list, rather then combining the
error-messages from all the models,
and keep my code concise?
From rails api doc
If you are going to modify the association (rather than just read from it), then it is a good idea to set the :inverse_of option on the source association on the join model. This allows associated records to be built which will automatically create the appropriate join model records when they are saved. (See the ‘Association Join Models’ section above.)
So simply add :inverse_of to relationship declaration (has_many, belongs_to etc) will make active_record save models in the right order.
The first thing that came to my mind - just get rid of that validation.
Second thing that came to mind - save the customer first and them build the contact.
Third thing: use :inverse_of when you declare the relationship. Might help as well.
You can save newly created related models in a single database transaction but not with a single call to save method. Some ORMs (e.g. LINQToSQL and Entity Framework) can do it but ActiveRecord can't. Just use ActiveRecord::Base.transaction method to make sure that either both models are saved or none of them. More about ActiveRecord and transactions here http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

Rails Has Many Association

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.

how to force a has_many to use a specific :foreign_key?

I'm having trouble with a model not honoring the :foreign_key policy.
Character model has the following fields:
name:string
level:int
realm:string
realm_id:integer
class Character < ActiveRecord::Base
belongs_to :realm
end
My Realms model looks like this:
class Realm < ActiveRecord::Base
has_many :characters, :foreign_key => "realm_id"
end
However, it seems like it's forcing the character model to use the :realm column as the foreign_key rather than :realm_id. I don't have any clue as to why or how to fix it. Is there any other way to make it ignore the :realm field and go for the :realm_id without having to change the name of the column?
[Edit for clarity]
The character model does have a realm_id:integer field. I have tried not having the foreign_key but the results with both is identical.
ruby-1.9.2-p136 :012 > c = Character.new
=> #
ruby-1.9.2-p136 :013 > c.realm = "Sargeras"
ActiveRecord::AssociationTypeMismatch: Realm(#2154038240) expected, got String(#2151988680)
Despite even having the foreign_key, it just refuses to let go of the realm column.
[Edit 2]
The realm column will just take over due to the has_many and belongs_to association. There is no way so far to break this, so the solution is to either remove the column (the approach i will take), or rename it to something different.
Did you make sure that your Character table has a realm_id column? Please make sure of that, and then get rid of foreign_key => 'realm_id, it is not necessary at all. Your program should work if you get both these things done.
You should not need the :foreign_key part here at all, since you're following the standard Rails naming convention, the realm_id column should be inferred from the model name.
EDIT
I see. I don't think you can have a column and an association by the same name in one model. The easiest solution would probably be to rename the "realm" column to "realmname" or something like that.
belongs_to :realm creates (among other things) methods called realm and realm= used as getters and setters. That means the method_missing magic that ActiveRecord uses to expose database columns as attributes is never triggered when you do Character#realm, as that method isn't in fact missing.
If you want to avoid renaming the realm column in your database, you could create attribute accessors for the field manually under another name:
class Character < ActiveRecord::Base
def realm_name
self['realm']
end
def realm_name=(value)
self['realm'] = value
end
end
This way you'll still have the realm column in your database and be able to access the attribute in Ruby, albeit under a different name. This isn't a great idea, though, as you'll be duplicating the realm name as both Character#realm_name and Character.realm.name.
I would ditch the realm column and instead make sure I use Realm objects when importing from the datasource:
character.realm = Realm.find_by_name('Sargeras')
That way you'd only realm data available where it makes sense; in the Realm model.

Write join table data - has_many :through

That should be a simple question but i can't find a good solution online.
I have three tables/models. User, Alliance and Alliance_Membership. The latter is a join table describing the :Alliance has_many :Users through :Alliance_Membership relationship.
Everything works ok, but Alliance_Membership now has an extra field called 'rank'. The question is, how do i set that when creating my new object ? Currently, i do something like :
#alliance.users << current_user
This is really convenient since it populates my Alliance_Membership table automatically. But, how can i set the Alliance_Membership.rank field as well ?
You'll need to create the membership yourself to set the 'rank' attribute. Something like this:
#alliance.alliance_memberships.create!(
:user => current_user,
:rank => 'whatever')

Resources