views:

178

answers:

4

I have several similar Models (ContactEmail, ContactLetter, ContactPostalcard).

Each instance (record) in, say, ContactEmail, means a specific Email template was sent to a specific Contact.

So, each one (e.g. ContactEmail) belongs_to :contact

So, in ContactEmail model, there is an attribute ContactEmail.contact_id.

Each Contact has a virtual attribute company_name.

So, when I write the following, I will get all emails sent within a specific set a conditions (in this case, time):

@sent_emails = ContactEmail.find(:all, :conditions => "conditions here")

So, @sent_emails.size would tell me the total of all emails sent.

My challenge: how do I extract more granularity, by unique company across the different models?

The output I want would look like the following:

FROM 8/1/10 TO 8/10/10 (where the dates are dynamic)

                Calls       Letter      Postalcards
Company 1         4           2             4
Company 2        10           4             6
Company 3         2           3             4

So Company3 has 2 Calls, which means there were two records of ContactCalls where the sent_date fell between the two dates, and where the associated contact belongs to Company 3.

Company 1-3 is not set ahead of time. It needs to be extracted from the pool of ContactCalls, ContactLetters, and ContactPostalcards.....

The challenge is that I don't know what the companies are. They are attributes of the contacts part of each distinct record. So in some cases, I may have Company2 has 0 letters.

Thanks for any guidance! :)

How I can find a company on a given record for ContactEmail model:

ContactEmail.contact.company.name

This will return the associated company for a specific ContactEmail

A: 

In my opinion, trying to extract this data in one query (which I suppose you want to do) is a little bit overkill. I'd issue one query for each type with :group => company_name option. Then I'd merge the results using Ruby code so I have some data structure that allows for easy display (probably a hash of hashes, e.g. {'Company 1' => {'Calls' => 5, 'Letter' => 0, 'Postalcards' => 3'}...}.

szeryf
what does the :group => option do? Will that give me the total for within a group (I guess similar to sql group, right?)
Angela
how do I search by company_name when company_name is an attribute of Contact, which each ContactEmail 'belongs_to"? Can I make a virtual attribute for each ContactEmail, ContactLetter that is also company_name?
Angela
How would I query across all the models (ContactEmail, ContactLetter, etc)
Angela
`:group => company_name` is the same as in SQL, but from other answers I see that you should rather use `:group => 'company_id'`.And as I said, doing it in one query is overkill IMHO, but you can see the code for that in Fullware's answer.
szeryf
I just want the easiest way to do it....company_name btw is a virtual attribute in contact, it has a belongs_to relationship....
Angela
A: 

If you really want to have all the contacts with the count of all these relationships, it might be a good idea to use a counter_cache.

http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html - find counter cache on this page

What it does is automatically keep a denormalized tracking of the count on each of contact's relationships, so you don't have to query information from all the tables grouping by the contact, which might incur in performance problems.

What you need to do is add columns 'emails_count', 'letters_count' and 'postcards_count' to your 'contacts' table, update it with the right count initially, and declare your has_many relationships as such:

class Contact < ActiveRecord::Base
  has_many :emails, :class_name => 'ContactEmail', :counter_cache => 'emails_count'
  # so on for other columns
end

With these setup, all you need to do is query your Contacts, and check their counter cache to render your table. It's number will be automatically updated by rails everytime an item is created or destroyed for that contact.

Marcos Toledo
if I wanted to reference, for example, additional information found in the ContactEmail model, I wouldn't, right? The counter-cache is just an aggregate number?
Angela
yeah, that's right. you could still, on each contact, access their contact_emails, but not without doing an extra query for each one internally.If you have some details in what kind of info you want to display on the aggregated columns, we might be able to help you further. PS. might be worth giving this some thought unless you want to load all the contents of all 4 tables simultaneously into memory. For tables that grow dynamically this is usually an idea to be avoided.
Marcos Toledo
agreed. So, let me think, my question displays what I want to show for now, so I uppose counter-cache could worki...except that I need to be able to filter by date. That number, then, by definition would change dynamically. right?
Angela
The same output has been populated with sample data...but the overall structure still the same ( the total count of ContactEmail records (these could be for the same Contact, btw) grouped by Company....thanks!
Angela
A: 

Oh, so I had misunderstood your question at first. I think I finally get what you mean.

So indeed, the only good way to do what you want (which is pull all the data from all those tables) is by issuing an include when querying for contacts:

@contacts = Contact.all :include => [:contact_emails, :contact_letters, :contact_postal_cards]

This will generate 4 queries, selecting all data on these 4 tables an populating your contacts accordingly, so you can navigate through the contact object's relationships any way you like to populate your table.

Finally, you mention your table would be grouped by companies and not contacts, so I assume multiple contacts could have the same 'company_name'. If that's the case, you can group your collection by the 'company_name' like this

@companies = @contacts.group_by &:company_name

This would create a hash like this:

{'Company 1' => [contact_1, contact_3], 'Company 2' => [contact_4]}

With those in place, you can generate your table with something like this:

<% @companies.each do |company_name, contacts| %>
    Company: <%= company_name %>
    <% contacts.each do |contact| %>
       Contact: <%= contact.name %>
          Emails : <%= contact.contact_emails.map(&:email).split(',') %>
          etc..
    <% end %>
<% end %>
Marcos Toledo
Hi, thanks --- sorry I got a little lost...how do I do the .count method? I have been playing around with the statistics gem, as well, which can give me the count of, say ContactEmails in a specific time-frame. Cool idea on company up with the hash...ContactEmail belongs to a Contact. is Contact above referring to the specific Model?
Angela
oh I see...so I could do contact.contact_emails.count ?
Angela
"is Contact above referring to the specific Model?" - yes, just like you do 'ContactEmail.all :conditions => {:contact_id => 1}' to get all emails from the first contact, 'Contact.all' will return all contacts in an array. Then you could get all emails from it with 'contacts.first.contact_emails'.
Marcos Toledo
"oh I see...so I could do contact.contact_emails.count ?" - actually you'd want to use "contact.contact_emails.size", since you've already loaded all the data when you first called 'Contact.all :include => :contact_emails'. That was me assuming you need more than the count (by your replies to my other answer). If you really only want the count, then my other answer is what you want (the counter cache)
Marcos Toledo
I see...the challenge is being able to aggregate them by company, not by specific Contact. I can do ContactEmail.Contact.Company.Name to generate the name of the company associated with any specific ContactEmail....does that help?
Angela
Marcos Toledo
Tell you what, I think you could just show us some real data from each of the models, and exactly what info you want to display on the table, or else this will just keep going back and forth.
Marcos Toledo
okay I agreed, let me past that....
Angela
I guess I'm confused by the fact your example has a "Contact:" output as if it wants to output specific Contacts. Check out my matrix -- I just need the number of ContactEmails sent within the specific time for the specific company (but the companies aren't known ahead of time...they are extracted from the ContactEmail information, for example, based on the Contact.
Angela
+2  A: 

This should do it nicely:

    list = Contact.find :all,
      :select => "companies.name company_name, COUNT(contact_emails.id) email_count, COUNT(contact_letters.id) letter_count, COUNT(contact_postalcards.id) postalcard_count",
      :joins => [
        "LEFT JOIN companies ON company.id = contact.company_id",
        "LEFT JOIN contact_emails ON contact_emails.contact_id = contacts.id",
        "LEFT JOIN contact_letters ON contact_letters.contact_id = contacts.id",
        "LEFT JOIN contact_postalcards ON contact_postalcards.contact_id = contacts.id"
      ],
      :group => "companies.id"

      list.each do |item|
        p item.company_name, item.email_count, item.letter_count, item.postalcard_count
      end
fullware
interesting....I will give it a try....
Angela
btw, company_name as noted above is a virtual attribute...so I don't think it works on selects?
Angela
yeah cann't fond company_name because not a column -- see the note I had...it has to be accessed through Contact.Company.Name
Angela
@Angela, then `group by` `contact_id`, and then map `contact_id`-s to company names.
Pavel Shved
For this solution to work, one has to use `OUTER JOIN`, i.e. `LEFT OUTER JOIN` instead of `LEFT JOIN`.
KandadaBoggu
LEFT JOIN is the same as LEFT OUTER JOIN.
fullware
Based on the nature of the question, I think perhaps the db schema was designed without having considered what functions it would need to support. I'm not sure I understand why you would want a virtual attribute such as company name and not have a mapping in the DB.
fullware
hmm...how should I change it? the reason is that a contact belongs_to a company but a company has_many contacts. that is how it is set up....what would you suggest?
Angela
Calling company_name a virtual attribute was misleading. It would be better to say that company is an association of contact, as your most recent comment. Since it is an association, it can be joined just like the other tables. See the updated code above. I think this is a good example to understand thoroughly. Note that the LEFT JOIN's mean that any contact without a company will be lumped into the same row. Be sure to understand why LEFT JOIN's are used here instead of the default (INNER) JOIN.
fullware
Actually, company_name is a virtual attribute. There is a way to get the association via contact.company.name, which we can do, but I created the virtual attribute for simplicity. I will need to review why LEFT JOIN...I *think* I get it...but let me give it a go, thakns!
Angela
I get an error: SQLite3::SQLException: no such column: company.id: SELECT companies.name company_name, COUNT(contact_emails.id) email_count, COUNT(contact_letters.id) letter_count, COUNT(contact_postalcards.id) postalcard_count FROM "contacts" LEFT JOIN companies ON company.id = contact.company_id LEFT JOIN contact_emails ON contact_emails.contact_id = contacts.id LEFT JOIN contact_letters ON contact_letters.contact_id = contacts.id LEFT JOIN contact_postalcards ON contact_postalcards.contact_id = contacts.id GROUP BY company.id
Angela
I made some adjustments and now get an error when it gets to p:p item.company_name, item.email_count, item.letter_count, item.postalcard_count
Angela
Hi, I get 'nil' -- doesn't the Inner Join pull a 'nil' if there is a nil result for one of the selects? So if there are no contact_letters at all, everything zeros out?
Angela
Corrected an error, company.id should have been companies.id
fullware
Not sure about the error with the "p" call. That's just for debug really, so you can see what it being produces. What's the error?
fullware
I was getting an eror so changed contact.company_id to contacts.company_id and company.id to companies.id
Angela
An INNER JOIN indeed will not load any rows unless there is a matching row for each side of the join. This is why I've put in LEFT JOIN instead. That means the table on the left, i.e. companies, will always be loaded, whether or not there are any matches in the other tables.
fullware
hmm...well the p say too many arguments, so I reduce the argument to only 1 item.company_name, but I get a nil
Angela
Well that's just debug code anyway so you can see the results, each element in the array will be an object of type Company, and those method calls are dynamically generated from the terms defined in the select. Everything looks right to me though. Try doing each one at a time.
fullware
Let's see...I went and made it item.inspect and it outputs #<Contact >
Angela
So I made changes anywhere in the join statement it was singular and pluralized since I assume it needs to reference a table name...just want to make sure....but, I am getting a nil, which shouldn't happen because there are records of contact_emails
Angela
hmm...the count looks right...it just doesn't seem to output the company name right...it comes out as "#<Contact >"
Angela
hmm...numbers don't seem to match...drilling down further into the data....
Angela
Right, each array element will be an instance of Contact (I incorrectly said Company before). Each item in the :select clause will be added as virtual attributes to each instance returned by the query. Give this a try: `list[0].attributes.inspect`, etc.
fullware
Interesting, list the attributes works pretty good: {"postalcard_count"=>3, "email_count"=>3, "letter_count"=>0, "company_name"=>"Typhoon Corporation"} but why am I getting a weird output when accessing item.company_name?
Angela
Oh you have a conflict between your virtual attribute method most likely. Change the :select to something else, and use that instead.
fullware
I see....should I remove the virtual attribute, I was trying to be slightly more simplistic than always calling contact.company.name but I don't know if the select will recognize it. is company_name just the value that is holding the outcome for that column from the select statement?
Angela
okay, cool, I think it was conflict with contact.company_name because item is a contact object so I changed the name of the select to co_name and it looks like it is working! I just need to validate the math but it seems right...thanks! hopefully no issues, but running it through production data to see it, thank you :)
Angela
odd behavior --- when I try to display all contact_emails, I see 2. But this shows a count_email to be 3, the same number as count_postalcard. Thoughts?
Angela
ah, something is odd -- I just sent another email to see what would happen...*both* postalcards *and* emails went to 5 instead of 4 for just count_email...sounds like something is being double-counted...?
Angela
I think the way it is either doing the count or the grouping is doing something funky...not sure how it is coming up with 5, when the actual number is 3 total, 2 for one contact, and 1 for another...so one contact is being double-counted....
Angela
Hi, is there a way we can figure out why it is counting things funky?
Angela
Sorry I don't get notified when there's a new comment unless I log in (come on StackOverflow!!). The best way to debug something like this is to find the SQL statement in the log file and execute it manually in the DB client. Then you could take off the GROUP BY entirely, and it would show you each row that it is selecting. You could tweak the columns in the SELECT so that you can find out what's going on. It will be worthwhile for you to do this exercise, it will be a very useful learning tool and something you'll use a lot in the future.
fullware