Rails associations for a fairly simple notification system - ruby-on-rails

I'm trying to set up a notification system in Rails, along with mongoid (but I don't think this is mongoid specific).
The basic structure is like this - Every notification has a notifier (the one who is responsible for the notification) and a notifee (the one who receives the notification). When a user A comments on user B's post (in a blog system for example), the User A becomes the notifier and the User B is the notifiee.
User.rb
# nothing in here
Notification.rb
has_one :notifier, :class_name => "User"
belongs_to :notifiee, :class_name => "User"
However, when I do:
#notification = Notification.new
#notification.notifier = current_user
#notification.notifiee = User.first #Just for the sake of the example
#notification.save
I get this error:
Problem: When adding a(n) User to Notification#notifier, Mongoid could
not determine the inverse foreign key to set. The attempted key was
'notifiee_id'.Summary: When adding a document to a relation, Mongoid
attempts to link the newly added document to the base of the relation
in memory, as well as set the foreign key to link them on the database
side. In this case Mongoid could not determine what the inverse
foreign key was.Resolution: If an inverse is not required, like a
belongs_to or has_and_belongs_to_many, ensure that :inverse_of => nil
is set on the relation. If the inverse is needed, most likely the
inverse cannot be figured out from the names of the relations and you
will need to explicitly tell Mongoid on the relation what the inverse
is.
What could I be doing wrong? Or, is there a better way to model this??
Any help is much appreciated! Thank you.

You should probably go for the following associations:
User:
has_many :notifications_as_notifier, :class_name=>'Notification', :foreign_key=>'notifier_id'
has_many :notifications_as_notifiee, :class_name=>'Notification', :foreign_key=>'notifiee_id'
Notification:
belongs_to :notifier, :class_name=>'User', :foreign_key=>'notifier_id'
belongs_to :notifiee, :class_name=>'User', :foreign_key=>'notifiee_id'
Your notifications table should have notifier_id and notifiee_id.
Now you can do,
#notification = Notification.new
#notification.notifier = current_user
#notification.notifiee = User.first #Just for the sake of the example
#notification.save
What I find questionable in your setup:
You have,
has_one :notifier, :class_name => "User"
belongs_to :notifiee, :class_name => "User"
When you use has_on, then the other relation (table) must have a foreign key referencing the parent. Here users must have a column notification_id or something. This is impractical because a single user has many notifications (based on your explanations).
Secondly, you are associating Notification to User through two relationships but you are mentioning anything about the foreign key to use to enforce the association.
And why do you not have an inverse relation in the User model? Would it not help if you had access to something like: current_user.notifications_as_notifier ??

Related

Rails - Associating an object with multiple accounts and then modifying the association

I have Credits that belong to Accounts, but are traded back and forth between accounts. I need to be able to keep track of which account a credit currently belongs to, and which account originally created the credit. Currently I have an association like this:
class Credit < ActiveRecord::Base
belongs_to :original_owner, :foreign_key => "account_id", :class_name => "Account"
belongs_to :account, :class_name => Account
end
Now in the rails console, I can set Credit.account.id = 3, and Credit.original_owner.id = 4, but that just changes the "Account id" and doesn't actually change the association. If I set Credit.account = Account.find(3), that works, but I can't set Credit.original_owner to Account.find(4). It indicates that it saved correctly, but it doesn't. It only changes with Credit.account. Every time I change the account, it changes the original_owner along with it.
How can I set up my association so that my Credits belong to an account but still keep track of their original account?
You can't set the foreign key for the original_owner association to account_id because that's the foreign key for the account association. You need to have a distinctly named foreign key for each association. (i.e. original_owner_id)
#credit.account.id = 3 and #credit.original_owner.id = 4 are not correct either. You're attempting to change the id of the associated objects. I assume what you're trying to is modify the association, which is done with #credit.account_id = 3 (using the foreign key), or, as you pointed out #credit.account = Account.find(3) also works if you want to work with objects.
You need to add a new column to your model and map foreign_key to that column.

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

How do I specify 2 foreign keys in a model in ruby on rails?

I'd like to be able to access current_users conversation with ease.
E.G.
current_user.conversations
The thing is the current user can either be a sender or recipient (sender_id or recipient_id)
class User < ActiveRecord::Base
has_many :conversations, :foreign_key => "sender_id"
This works but there are times when a sender is a recipient or say for example someone has sen t someone 10 messages as a sender and the recipient hasn't replied to any but still a conversation exists in the database for them.
If I was logged in as that user and typed current_user.conversations nothing would come up because the foreign key specified in the user model is sender_id. Isn't there a way to specify both? Like using "OR" in sql?
Kind regards
I don't believe there is a way to do exactly what you want, but there are a few alternate approaches you may want to consider:
1) Splitting it up into two relationships, sent_conversations and received_conversations, and then you can define a method which combines them.
2) Adding a new class method that does some custom SQL:
class User < ActiveRecord::Base
def conversations
Conversation.where("sender_id = :id OR receiver_id = :id", id: self.id)
end
end

Get data from database with two conditions in one list

I'm new to this. I have a (sqlite3, but with ActiveRecord it doesn't matter) table called Messages and a model called Message. I want to find all messages in database that have user_id or reciever_id equal to the object user and his attribute id (for short user.id). I know it's probably just one simple line of code, but I wanna do it the right "rails" way and I don't have much experience with this.
I'm using Rails 3. Thanks for any help.
Cheers
I suspect that what you will want this relationship in many places in your code, and actually this represents a fundamental part of your application's design.
Conceptually a 'message' belongs to a 'sender' and also to a 'receiver'. In reverse, a 'user' has many messages that she has sent, and many messages that she has received.
in the Message model, add the following
belongs_to :user
belongs_to :receiver, :class_name => "User"
in the User model, add the following
has_many :messages
has_many :sent_or_received_messages, :class_name => "Message", :conditions => ["user_id = ? OR receiver_id = ?", id, id])
Now you can do this:
my_user.messages # all of the messages the user has sent
my_message.user # the user who sent the message
my_message.receiver # the user who received the message
my_user.sent_or_received_messages # all messages where the user was a sender or a receiver
I'm assuming that you mean a user has_many :messages....
# assuming you are looking for a particular user
Message.where(['user_id=? OR receiver_id=?', user.id, user.id])

Rails has_many_polymorphs in reverse?

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.

Resources