I have the following code which when run creates / deletes records on table project_users using a has many through relationship and this works fine.
#project.users = #account.users.find(params[:users_ids])
However, i'm now in a situation where I need to set a 3rd foreign key value on this middle project_users table called role_id.
Are there any suggestions to efficiently set the role_id at the same time as the code above.
I've come up with a couple of solutions such as the one below, but I don't like the idea of deleting all records if only an update is required.
users = #account.users.find(params[:users_granted].keys)
#project.users = []
users.each do |user|
#project.project_users.create(:user_id => user.id, :project_role_id => params[:users_granted][user.id.to_s])
end
Any thoughts / suggestions would be welcome, chances are i'm thinking about this the completely wrong way!
Thanks,
Scott
Related
Over a period of time my Rails app has had various rewrites, and in some cases incorrect model associations.
Currently my User model has_many :posts and its destroy method correctly removes all dependent Posts, but at times when things were improperly written this did not happen. I'm now left with a handful of Post records that cause errors all over because their User does not exist.
What would be the most efficient way to manually filter through all Post records, check if its User actually exists, and if not destroy that Post?
I'm imagining something like:
Post.all.select{ |post| post.user.nil? }.destroy
But that seems incredibly inefficient for thousands of records. I'd love to know the best way to do this. Thank you!
any reason why you cannot do it directly on the database?
delete from posts where user_id not in (select id from users);
Fastest way would probably be to do it directory in the db console, but if you've got other dependent relationships and activerecord callbacks that you need to get fired, you could do something like:
Post.where("id in (select p.id from posts p left outer join users u on p.user_id = u.id where u.id is null)").destroy_all
Delete the orphan posts using
If you want to destroy the dependent associations with Post and run the callbacks
Post.where("user_id NOT IN (?)", User.pluck(:id)).destroy_all
If you just want to delete the posts
Post.where("user_id NOT IN (?)", User.pluck(:id)).delete_all
Here is one good post about finding and deleting orphan records
My two models are User has_many :books and Book belongs_to :user.
The users table has only the column name, while the books table has users_id and title.
Is this how I'm actually supposed to use them? With the users table populated, how do I go about adding a book with a specific user, done by searching their name and not the ID column? I know this is a simple thing, but I really cannot find it on Google, or by re-reading my books and re-watching my Lynda videos; I know the information must be in there somewhere but it is completely frying my brain right now, and I'm extremely confused. I'm very used to SQL and learning to use ActiveRecord instead feels like trying to write with my left hand.
What I want to do is the equivalent of, in SQL, INSERT INTO books (title, users_id) VALUES ("Wolves of the Calla", (SELECT id FROM users WHERE name = 'Sarah'));.
Find the user with the given name and then use the association to create a book with the found user_id
user = User.where(:name => "Sarah").first
user.books.create(:title => "Wolves of the Calla")
As explained in the Association Basics Guide, you'd need something like this:
createdBook = #user.books.create(title: "Wolves of the Calla")
This can all be found in the Rails Documentation. Its worth a read, if you haven't done so already.
In regards to this question:
...how do I go about adding a book with a specific user...
There are a number of ways you can do it, but the key thing to remember is that the has_many and belongs_to methods "create" association methods for you. In this way you can retrieve, and add to an object's associations; Rails will take care of handling the foreign keys and such, so long as they are named in accordance with its naming convention (which it seems you have done)
So as a simple example, to add a book to a user's collection of books, would be something like this:
#user = User.where(name: "Sarah").first
#user.books.create(title: "Wolves of the Calla")
#Rails 4 syntax
#Approach 1
#user = User.find_by(name: 'Sarah')
#book = #user.books.create(title: 'Wolves of the Calla')
#Alternatively
#user = User.find_by(name: 'Sarah')
#user.books << Book.create(title: 'Wolves of the Calla')
#Alternatively
#user = User.find_by(name: 'Sarah')
#book = Book.create(title: 'Wolves of the Calla')
#user.book_ids += [#book.id]
I'm trying to set the order for Topic index by most recent Comment. This is working:
Topic.joins(:comments).order("comments.created_at desc")
But it lists the Topics more than one time.
Is there a way to limit the times each topic is displayed to one?
Ok, what's happening is this: When you join the comments, you get one database row for each comment, which (if there are multiple comments on a Topic), means multiple copies of each topic record.
To fix this, you'll need to use grouping, so that there's only one result per topic. The thing to group by is the id of the model you're returning (topics.id). Now, there's more to it - because there are still multiple comments per result, there are also multiple created_at values for each, and thus (in order to sort by it, as you do) you need to tell the database which one to use.
This is done with an aggregation function of some sort. I'm guessing you want the most recent comment to be the one that determines the order. If that's true, the code will be something like this:
Topic.joins(:comments).select('topics.*, max(comments.created_at) as last_comment').group('topics.id').order('last_comment desc')
The custom select includes the usual data that you need (everything about the Topic object - topics.*) and also an aggregate function (max(), which as the name suggests returns the largest of the possible values. ) used on the creation date of the comments. More recent dates are larger, so this will be the most recent comment's creation datestamp. That result is aliased as last_comment (using as), so you can refer to it in the .order call.
The most elegant way I found to solve the problem is to add touch to the polymorphic association in the comment model:
belongs_to :commentable, :polymorphic => true, touch: true
If you're not using a polymorphic association, you can use:
belongs_to :topic, touch: true
Then, all I had to do was change the default scope in the topic model to updated_at
default_scope order: 'topics.updated_at DESC'
Adding touch to comment means that every time a comment is added to topic, it updates the updated_at column in topics.
In the controller I am using:
#topics = Topic.order(sort_column + " " + sort_direction).paginate(:per_page => 20, :page => params[:page])
Topic.all or something else could work there as well.
Thanks to my super friend Alain for pointing this out to me.
It occurred to me that if I have a has_many join, where the foreign model does not have a belongs_to, and so the join is one way, then I don't actually need a foreign key.
We could have a column, category_ids, which stores a marshaled Array of IDs which we can pass to find.
So here is an untested example:
class page < AR
def categories
Category.find(self.category_ids)
end
def categories<<(category)
# get id and append to category_ids
save!
end
def category_ids
#cat_ids ||= Marshal.load(read_attribute(:category_ids)) rescue []
end
def category_ids=(ids)
#cat_ids = ids
write_attribute(:category_ids, ids)
end
end
page.category_ids => [1,4,12,3]
page.categories => Array of Category
Is there accepted pattern for this already? Is it common or just not worth the effort?
Wouldn't performance suffer here when you are marshalling / unmarshalling?
I personally don't think this is worth the effort and what you are trying to do doesn't seem clear.
Actually this looks like a many-to-many mapping rather than a many-to-one, as there is no code that prevents a category from belonging to more than one page, surely you want something like:
create table categories_pages (
category_id integer not null references categories(id),
page_id integer not null references pages(id),
primary_key(category_id, page_id)
);
with either a has and belongs to many on both sides or has_many :through on both sides (depending on whether you want to store more stuff).
I agree with Omar that this doesn't seem to be worth the effort.
Your database no longer reflects your data model and isn't going to provide any help in enforcing this relationship. Also, you now have to keep your marshalled id array in sync with the Category table and enforce uniqueness across Pages if you want to restrict the relationship to has_many.
But I guess most importantly, what is the benefit of this approach? It is going to increase complexity and increase the amount of code you have to write.
I'm working with models analogous to the following:
class Owner < ActiveRecord::Base
has_many :owned
end
class Owned < ActiveRecord::Base
belongs_to :owner
end
You can presume that owned_id and owner_id are in the right places. The trouble is that, in a controller for a different mvc chain in the app,
#owner = Owned.find_by_id(owned_id, :include => :owner)
doesn't work. I get the owner_id, column, naturally, but can't then do
#owned.owner # is just nil
What gives? I mean, I could do the assignment directly before passing the result on to the view:
#owned.owner = Owner.find_by_id(#owned.owner_id)
but that just seems silly. Come on, embarrass me. What's the obvious thing that I've missed? This works in other places in my app, but I can't spot the differences. Are there some common traps? Anything helps.
Thank you
I just keep winning. The corresponding 'Owner' object had been deleted from the owners table.
The funny thing is, before I created an account, I had tons of karma on my cookie-based identity. Then my cookies became corrupted, and I can't ask anything but stupid questions anymore, and my karma sits at 1. Oh well.
Reputation on StackOverflow is not cookie based. You may have to log in again or something.
Your question seems to imply that you have an owned_id field in the owner table. You don't need that and should remove it.
You just need an owner_id integer field in the owned table.
You can access your records and relationships in a number of ways. First let's start by accessing the owner record first.
owner = Owner.find(owner_id)
owned = owner.owned # this is an array since you a 'has_many' relationship
Normally you'd want to access the owned records in the following way:
for owned in owner.owned
puts owned.name # or access any other attributes
end
If you would like to access the owned records first you could do the following:
#owned = Owned.find(:all, :conditions => [ "owner_id = ?", owner_id ])
# #owned is an array so you need to iterate through it
for owned in #owned
puts owned.owner.name # or access any other attribute from the owner
end
Once you've got these queries working fine you can worry about eager loading by adding :include in your find statements. Note that this can be of interest for optimization but not necessary from the get go.
I hope this helps.