There are 4 models.(Fashion,FashionUnderwear,Underwear,Brand).Each user can select his own style from the fashion form.The fashion form has various item's drop boxes such as jeans and socks.I wanted to rearrange the items classified by brand in the Drop box alphabetically, so I used the scope.
#### Underwear dropbox
Adidas
adidas underwear 1
adidas underwear 2
NIKE
NIKE underwear 1
NIKE underwear 2
NIKE underwear 3
It worked well with the scope, but now I got a warning that you should include the scope.
In the code below, opening Fashions/new.html will give me a warning "Please Include UnderwearNameAsc".
I studied various things and tried it, but in all cases using child models, I can not find a clue to solve.
### Fashion model
has_one :fashion_underwear
accepts_nested_attributes_for :fashion_underwear
### FashionUnderwear model(Intermediate table)
belongs_to :fashion
belongs_to :underwear
### Underwear model
has_many :fashion_underwears
belongs_to :brand
scope :UnderwearNameAsc, -> { order(UnderwearName: :asc) }
### Brand model
has_many :underwear
has_many :UnderwearNameAsc, -> { order(UnderwearName: :asc) }, class_name: 'Underwear'
### Fashions.controller
def new
#fashion = Fashion.new
#fashion.build_fashion_underwear
#brand = Brand.includes(:fashion_underwears).joins(:fashion_underwears).order(brand_name: :asc)
end
### Fashion/new.html
= simple_form_for(#fashion) do |f|
= f.simple_fields_for :fashion_underwear do |p|
= p.input :underwear_id, collection: #brand, as: :grouped_select, group_method: :UnderwearNameAsc, group_label_method: :brand_name, label_method: :UnderwearName
Bullet is pointing out that you have an n+1 query problem. It suggests that you change your has_many association within the Brand model by adding an includes to solve the problem, like this:
has_many :UnderwearNameAsc, -> { includes(:underwear).order(UnderwearName: :asc) }, class_name: 'Underwear'
While you are at it, you really should change the names of your scopes and methods to snake_case in standard Ruby style. Otherwise Ruby will think those methods and scopes are constants.
Also, I would just name your scope for what it is returning, not for a single attribute on that model. Finally, I'd use a scope separately and call it explicitly when you want the resources to be returned sorted.
has_many :underwear # Or has_many :underwears if Rails interprets the plural that way
scope :sorted, -> { includes(:underwear).order(underwear: {name: :asc} }
Then you can do:
Brand.all # Returns unsorted records
Brand.all.sorted # Returns records sorted by underwear.name
Related
Using Rails 6 I am designing an application to manage police fines. A user can violate many articles, an article can have many letters and a letter can have many commas.
This is my implementation:
#models/fine.rb
class Fine < ApplicationRecord
has_many :violations
has_many :articles, through: :violations
has_many :letters, through: :violations
has_many :commas, through: :violations
end
#models/article.rb
class Article < ApplicationRecord
has_many :letters
has_many :violations
has_many :fines, through: :violations
end
#models/letter.rb
class Letter < ApplicationRecord
belongs_to :article
has_many :commas
has_many :violations
has_many :fines, through: :violations
end
#models/comma.rb
class Comma < ApplicationRecord
belongs_to :letter
has_many :violations
has_many :fines, through: :violations
end
#models/violation.rb
class Violation < ApplicationRecord
belongs_to :fine
belongs_to :article
belongs_to :letter, optional: true
belongs_to :comma, optional: true
end
When I print the fine in PDF I need to show violations: articles, letters and commas. I have difficulty creating a form to compile the fine because it is too deep. I am using Active Admin, when I create a new fine I want to associate many violations.
Violation example:
Violation.new
=> #<Violation id: nil, article_id: nil, fine_id: nil, letter_id: nil, comma_id: nil, note: nil, created_at: nil, updated_at: nil>
How can I create a form (using Active Admin, which uses Formtastic) to associate many violations to a fine? Example form:
Example (with sample data):
Violation.new fine: fine, article: a, letter: a.letters.last, comma: a.letters.second.commas.last
=> #<Violation id: nil, article_id: 124, fine_id: 66, letter_id: 10, comma_id: 4, note: nil, created_at: nil, updated_at: nil>
In my humble opinion, your question is rather vague and difficult to answer based only on the provided information. Since I can't produce an answer that will definitely solve your issue, allow me to try and point you in the right direction.
Rendering the form
First let's understand the problem here: you're trying to create an association record in a nested resource form.
You need to customize the form for Fine to include a form for each violation. Look at how ActiveAdmin handles nested resources. It should be something like:
ActiveAdmin.register Fine do
form do |f|
inputs 'Violations' do
f.has_many :violations do |vf|
vf.input :article, as: :select, collection: Article.all
vf.input :letter, as: :select, collection: Letter.all
vf.input :comma, as: :select, collection: Comma.all
end
end
end
end
Put simply, this is the answer to your question "How can I create a form (using Active Admin, who use Formtastic) to associate many violations to a Fine?".
Caveats
As you probably already noticed, there are a couple of problems with this approach.
First, it is nothing like your example. You can easily change things for Formtastic to add the check-boxes by using as: :check_boxes, but you'll find the check-boxes are not organized as you want with that pretty indentation. As far as I know, there is no way for you to do this with Formtastic. Instead, I believe you would have to use a partial.
Using a partial you can easily go through the articles, and render a check-box for each of them and go through each one's letters, and so on. However, bear in mind this form will require you to customize the controller so it understands each of these check-boxes and creates the respective violations. Not as straight forward.
Second, there is nothing enforcing the data integrity here. One could select an article, the letter of another one, and the comma of a third one (by the way, I hope you have a validation to protect you from this). To have the form dynamically change, so only the letters of a given article are shown after its selection, and same thing for the commas, would require some client-side logic. Not worth the trouble if you ask me.
Conclusion
Your question is far from simple and obvious, both to answer and to solve. One option you always have is a custom set of routes for managing such resources outside ActiveAdmin. Remember, tools like this are only as valuable as the work they take from you. If you're having to fight it, better to just step out of each other's way.
Hope this helps, in any way.
Solved:
f.has_many :violations do |vf|
vf.input :article, as: :select, include_blank: false, collection: options_for_select(Article.all.map {|article| [article.number, article.id, { :'data-article-id' => article.id, :'data-letters' => article.letters.map(&:id).to_json }]})
vf.input :letter, as: :select, collection: options_for_select(Letter.all.map {|letter| [letter.letter, letter.id, { :'hidden' => true, :'data-letter-id' => letter.id, :'data-article-id' => letter.article.id, :'data-commas' => letter.commas.map(&:id).to_json }]})
vf.input :comma, as: :select, collection: options_for_select(Comma.all.map {|comma| [comma.number, comma.id, { :'hidden' => true, :'data-comma-id' => comma.id, :'data-letter-id' => comma.letter.id }]})
end
And with a bit of javascript:
$(document).on('has_many_add:after', '.has_many_container', function (e, fieldset, container) {
selects = fieldset.find('select');
article_select = selects[0];
letter_select = selects[1];
comma_select = selects[2];
$(article_select).on("change", function () {
$(letter_select).prop('selectedIndex', 0);
$(comma_select).prop('selectedIndex', 0);
$("#" + letter_select.id + " option").prop("hidden", true);
$("#" + comma_select.id + " option").prop("hidden", true);
letters = $(this).find(':selected').data('letters');
$.each(letters, function (index, id) {
$("#" + letter_select.id + " option[data-letter-id='" + id + "']").removeAttr("hidden");
});
});
$(letter_select).on("change", function () {
$(comma_select).prop('selectedIndex', 0);
$("#" + comma_select.id + " option").prop("hidden", true);
commas = $(this).find(':selected').data('commas');
$.each(commas, function (index, id) {
$("#" + comma_select.id + " option[data-comma-id='" + id + "']").removeAttr("hidden");
});
});
});
I show all Articles, Letters and Commas in the selectbox. Initially Commas and Letters are hidden, then when a User click an Article the Letter's selectbox show only the related Letters.
The Commas code works same as Letters.
After I can add some validations in the Violation model.
Consider a simple association...
class Person
has_many :friends
end
class Friend
belongs_to :person
end
What is the cleanest way to get all persons that have NO friends in ARel and/or meta_where?
And then what about a has_many :through version
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
end
class Friend
has_many :contacts
has_many :people, :through => :contacts, :uniq => true
end
class Contact
belongs_to :friend
belongs_to :person
end
I really don't want to use counter_cache - and I from what I've read it doesn't work with has_many :through
I don't want to pull all the person.friends records and loop through them in Ruby - I want to have a query/scope that I can use with the meta_search gem
I don't mind the performance cost of the queries
And the farther away from actual SQL the better...
Update 4 - Rails 6.1
Thanks to Tim Park for pointing out that in the upcoming 6.1 you can do this:
Person.where.missing(:contacts)
Thanks to the post he linked to too.
Update 3 - Rails 5
Thanks to #Anson for the excellent Rails 5 solution (give him some +1s for his answer below), you can use left_outer_joins to avoid loading the association:
Person.left_outer_joins(:contacts).where(contacts: { id: nil })
I've included it here so people will find it, but he deserves the +1s for this. Great addition!
Update 2
Someone asked about the inverse, friends with no people. As I commented below, this actually made me realize that the last field (above: the :person_id) doesn't actually have to be related to the model you're returning, it just has to be a field in the join table. They're all going to be nil so it can be any of them. This leads to a simpler solution to the above:
Person.includes(:contacts).where(contacts: { id: nil })
And then switching this to return the friends with no people becomes even simpler, you change only the class at the front:
Friend.includes(:contacts).where(contacts: { id: nil })
Update
Got a question about has_one in the comments, so just updating. The trick here is that includes() expects the name of the association but the where expects the name of the table. For a has_one the association will generally be expressed in the singular, so that changes, but the where() part stays as it is. So if a Person only has_one :contact then your statement would be:
Person.includes(:contact).where(contacts: { person_id: nil })
Original
Better:
Person.includes(:friends).where(friends: { person_id: nil })
For the hmt it's basically the same thing, you rely on the fact that a person with no friends will also have no contacts:
Person.includes(:contacts).where(contacts: { person_id: nil })
smathy has a good Rails 3 answer.
For Rails 5, you can use left_outer_joins to avoid loading the association.
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Check out the api docs. It was introduced in pull request #12071.
This is still pretty close to SQL, but it should get everyone with no friends in the first case:
Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Persons that have no friends
Person.includes(:friends).where("friends.person_id IS NULL")
Or that have at least one friend
Person.includes(:friends).where("friends.person_id IS NOT NULL")
You can do this with Arel by setting up scopes on Friend
class Friend
belongs_to :person
scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) }
scope :to_nobody, ->{ where arel_table[:person_id].eq(nil) }
end
And then, Persons who have at least one friend:
Person.includes(:friends).merge(Friend.to_somebody)
The friendless:
Person.includes(:friends).merge(Friend.to_nobody)
Both the answers from dmarkow and Unixmonkey get me what I need - Thank You!
I tried both out in my real app and got timings for them - Here are the two scopes:
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") }
scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") }
end
Ran this with a real app - small table with ~700 'Person' records - average of 5 runs
Unixmonkey's approach (:without_friends_v1) 813ms / query
dmarkow's approach (:without_friends_v2) 891ms / query (~ 10% slower)
But then it occurred to me that I don't need the call to DISTINCT()... I'm looking for Person records with NO Contacts - so they just need to be NOT IN the list of contact person_ids. So I tried this scope:
scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }
That gets the same result but with an average of 425 ms/call - nearly half the time...
Now you might need the DISTINCT in other similar queries - but for my case this seems to work fine.
Thanks for your help
Unfortunately, you're probably looking at a solution involving SQL, but you could set it in a scope and then just use that scope:
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0")
end
Then to get them, you can just do Person.without_friends, and you can also chain this with other Arel methods: Person.without_friends.order("name").limit(10)
A NOT EXISTS correlated subquery ought to be fast, particularly as the row count and ratio of child to parent records increases.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
Also, to filter out by one friend for instance:
Friend.where.not(id: other_friend.friends.pluck(:id))
Here is an option using a subquery:
# Scenario #1 - person <-> friend
people = Person.where.not(id: Friend.select(:person_id))
# Scenario #2 - person <-> contact <-> friend
people = Person.where.not(id: Contact.select(:person_id))
The above expressions should generate the following SQL:
-- Scenario #1 - person <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT friends.person_id
FROM friends
)
-- Scenario #2 - person <-> contact <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT contacts.person_id
FROM contacts
)
As a simple example, let's say a bookstore has books which have one author. The books has many sales through orders. Authors can have many books.
I am looking for a way to list the authors ordered by sales. Since the sales are associated with books, not authors, how can I accomplish this?
I would guess something like:
Author.order("sales.count").joins(:orders => :sales)
but that returns a column can't be found error.
I have been able to connect them by defining it in the Author model. The following displays the correct count for sales, but it does ping the database for each and every author... bad. I'd much rather eager load them, but I can't seem to get it to work properly since it will not list any authors who happen to have 0 sales if I remove the self.id and assign the join to #authors.
class Author < ActiveRecord::Base
def sales_count
Author.where(id: self.id).joins(:orders => :sales).count
end
end
And more specifically, how can I order them by the count result so I can list the most popular authors first?
Firstly, let's have all associations available on the Author class itself to keep the query code simple.
class Author < AR::Base
has_many :books
has_many :orders, :through => :books
has_many :sales, :through => :orders
end
The simplest approach would be for you to use group with count, which gets you a hash in the form {author-id: count}:
author_counts = Author.joins(:sales).group("authors.id").count
=> {1 => 3, 2 => 5, ... }
You can now sort your authors and lookup the count using the author_counts hash (authors with no sales will return nil):
<% Author.all.sort_by{|a| author_counts[a.id] || 0}.reverse.each do |author| %>
<%= author.name %>: <%= author_counts[author.id] || 0 %>
<% end %>
UPDATE
An alternative approach would be to use the ar_outer_joins gem that allows you get around the limitations of using includes to generate a LEFT JOIN:
authors = Author.outer_joins(:sales).
group(Author.column_names.map{|c| "authors.#{c}").
select("authors.*, COUNT(sales.id) as sales_count").
order("COUNT(sales.id) DESC")
Now your view can just look like this:
<% authors.each do |author| %>
<%= author.name %>: <%= author.sales_count %>
<% end %>
This example demonstrates how useful a LEFT JOIN can be where you can't (or specifically don't want to) eager load the other associations. I have no idea why outer_joins isn't included in ActiveRecord by default.
I am trying to get a list, and I will use books as an example.
class Book < ActiveRecord::Base
belongs_to :type
has_and_belongs_to_many :genres
end
class Genre < ActiveRecord::Base
has_and_belongs_to_many :books
end
So in this example I want to show a list of all Genres, but it the first column should be the type. So, if say a genre is "Space", the types could be "Non-fiction" and "Fiction", and it would show:
Type Genre
Fiction Space
Non-fiction Space
The Genre table has only "id", "name", and "description", the join table genres_books has "genre_id" and "book_id", and the Book table has "type_id" and "id". I am having trouble getting this to work however.
I know the sql code I would need which would be:
SELECT distinct genres.name, books.type_id FROM `genres` INNER JOIN genres_books ON genres.id = genres_books.genre_id INNER JOIN books ON genres_books.book_id = books.id order by genres.name
and I found I could do
#genre = Genre.all
#genre.each do |genre|
#type = genre.book.find(:all, :select => 'type_id', :group => 'type_id')
#type.each do |type|
and this would let me see the type along with each genre and print them out, but I couldn't really work with them all at once. I think what would be ideal is if at the Genre.all statement I could somehow group them there so I can keep the genre/type combinations together and work with them further down the road. I was trying to do something along the lines of:
#genres = Genre.find(:all, :include => :books, :select => 'DISTINCT genres.name, genres.description, books.product_id', :conditions => [Genre.book_id = :books.id, Book.genres.id = :genres.id] )
But at this point I am running around in circles and not getting anywhere. Do I need to be using has_many :through?
The following examples use your models, defined above. You should use scopes to push associations back into the model (alternately you can just define class methods on the model). This helps keep your record-fetching calls in check and helps you stick within the Law of Demeter.
Get a list of Books, eagerly loading each book's Type and Genres, without conditions:
def Book < ActiveRecord::Base
scope :with_types_and_genres, include(:type, :genres)
end
#books = Book.with_types_and_genres #=> [ * a bunch of book objects * ]
Once you have that, if I understand your goal, you can just do some in-Ruby grouping to corral your Books into the structure that you need to pass to your view.
#books_by_type = #books.group_by { |book| book.type }
# or the same line, more concisely
#books_by_type = #books.group_by &:type
#books_by_type.each_pair do |type, book|
puts "#{book.genre.name} by #{book.author} (#{type.name})"
end
I have the following models. Users have UserActions, and one possible UserAction can be a ContactAction (UserAction is a polymorphism). There are other actions like LoginAction etc. So
class User < AR::Base
has_many :contact_requests, :class_name => "ContactAction"
has_many :user_actions
has_many_polymorphs :user_actionables, :from => [:contact_actions, ...], :through => :user_actions
end
class UserAction < AR::Base
belongs_to :user
belongs_to :user_actionable, :polymorphic => true
end
class ContactAction < AR::Base
belongs_to :user
named_scope :pending, ...
named_scope :active, ...
end
The idea is that a ContactAction joins two users (with other consequences within the app) and always has a receiving and a sending end. At the same time, a ContactAction can have different states, e.g. expired, pending, etc.
I can say #user.contact_actions.pending or #user.contact_requests.expired to list all pending / expired requests a user has sent or received. This works fine.
What I would now like is a way to join both types of ContactAction. I.e. #user.contact_actions_or_requests. I tried the following:
class User
def contact_actions_or_requests
self.contact_actions + self.contact_requests
end
# or
has_many :contact_actions_or_requests, :finder_sql => ..., :counter_sql => ...
end
but all of these have the problem that it is not possible to use additional finders or named_scopes on top of the association, e.g. #user.contact_actions_or_requests.find(...) or #user.contact_actions_or_requests.expired.
Basically, I need a way to express a 1:n association which has two different paths. One is User -> ContactAction.user_id, the other is User -> UserAction.user_id -> UserAction.user_actionable_id -> ContactAction.id. And then join the results (ContactActions) in one single list for further processing with named_scopes and/or finders.
Since I need this association in literally dozens of places, it would be a major hassle to write (and maintain!) custom SQL for every case.
I would prefer to solve this in Rails, but I am also open to other suggestions (e.g. a PostgreSQL 8.3 procedure or something simliar). The important thing is that in the end, I can use Rails's convenience functions like with any other association, and more importantly, also nest them.
Any ideas would be very much appreciated.
Thank you!
To provide a sort-of answer to my own question:
I will probably solve this using a database view and add appropriate associations as needed. For the above, I can
use the SQL in finder_sql to create the view,
name it "contact_actions_or_requests",
modify the SELECT clause to add a user_id column,
add a app/models/ContactActionsOrRequests.rb,
and then add "has_many :contact_actions_or_requests" to user.rb.
I don't know how I'll handle updating records yet - this seems not to be possible with a view - but maybe this is a first start.
The method you are looking for is merge. If you have two ActiveRecord::Relations, r1 and r2, you can call r1.merge(r2) to get a new ActiveRecord::Relation object that combines the two.
If this will work for you depends largely on how your scopes are set up and if you can change them to produce a meaningful result. Let's look at a few examples:
Suppose you have a Page model. It has the normal created_at and updated_at attributes, so we could have scopes like:
:updated -> { where('created_at != updated_at') }
:not_updated -> { where('created_at = updated_at') }
If you pull this out of the database you'll get:
r1 = Page.updated # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at)
r2 = Page.not_updated # SELECT `pages`.* FROM `pages` WHERE (created_at = updated_at)
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at) AND (created_at = updated_at)
=> []
So it did combine the two relations, but not in a meaningful way. Another one:
r1 = Page.where( :name => "Test1" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test1'
r2 = Page.where( :name => "Test2" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
So, it might work for you, but maybe not, depending on your situation.
Another, and recommended, way of doing this is to create a new scope on you model:
class ContactAction < AR::Base
belongs_to :user
scope :pending, ...
scope :active, ...
scope :actions_and_requests, pending.active # Combine existing logic
scope :actions_and_requests, -> { ... } # Or, write a new scope with custom logic
end
That combines the different traits you want to collect in one query ...