ActiveRecord .select(): Possible to clear old selects? - ruby-on-rails-3

Is there a way to clear old selects in a .select("table.col1, ...") statement?
Background:
I have a scope that requests accessible items for a given user id (simplified)
scope :accessible, lambda { |user_id|
joins(:users).select("items.*")
.where("items_users.user_id = ?) OR items.created_by = ?", user_id, user_id)
}
Then for example in the index action i only need the item id and title, so i would do this:
#items = Item.accessible(#auth.id).select("polls.id, polls.title")
However, this will select the columns "items., items.id, items.title". I'd like to avoid removing the select from the scope, since then i'd have to add a select("items.") everywhere else. Am I right to assume that there is no way to do this, and i either live with fetching too many fields or have to use multiple scopes?

Fortunately you're wrong :D, you can use the #except method to remove some parts of the query made by the relation, so if you want to remove the SELECT part just do :
#items = Item.accessible(#auth.id).except(:select).select("polls.id, polls.title")

reselect (Rails 6+)
Rails 6 introduced a new method called reselect, which does exactly what you need, it replaces previously set select statement.
So now, your query can be written even shorter:
#items = Item.accessible(#auth.id).reselect("polls.id, polls.title")

Related

ActiveRecord outputs too much in select query for Rails

According to:
#actors = actors.to_sql
this query:
actors = Actor.select("actors.name")
supposed to be equal to:
SELECT actors.name FROM `actors`
And it works in my SQL; only selects actors.name ... so i try:
#actors = actors.to_json.to_s
and render on some view... I get this;
[{"name":"James Patrick Pe","id":null,"logo":{"url":"uploads/default.jpg","topbar_avatar":{"url":"uploads/topbar_avatar_default.jpg"}}},{"name":"SSS","id":null,"logo":{"url":"uploads/default.jpg","topbar_avatar":{"url":"uploads/topbar_avatar_default.jpg"}}},{"name":"Philhealth","id":nul ....
it's basically a JSON of everything... I only need one column for an AJAX reply. How can I do this with the ActiveRecord Way?
Do you use Paperclip? It's likely you have a method that appends something to your JSON.
What you need to do is to specify the fields you want to be exported.
#actors = actors.to_json(only: [:name]).to_s
Also please note that there is a better approach to to_json. You should definitely have serializers that take an object or collection in input and deal with the serialization.

Trying to refactor Rails 4.2 scope

I have updated this question
I have the following SQL scope in a RAILS 4 app, it works, but has a couple of issues.
1) Its really RAW SQL and not the rails way
2) The string interpolation opens up risks with SQL injection
here is what I have:
scope :not_complete -> (user_id) { joins("WHERE id NOT IN
(SELECT modyule_id FROM completions WHERE user_id = #{user_id})")}
The relationship is many to many, using a join table called completions for matching id(s) on relationships between users and modyules.
any help with making this Rails(y) and how to set this up to take the arg of user_id with out the risk, so I can call it like:
Modyule.not_complete("1")
Thanks!
You should have added few info about the models and their assocciation, anyways here's my trial, might have some errors because I don't know if the assocciation is one to many or many to many.
scope :not_complete, lambda do |user_id|
joins(:completion).where.not( # or :completions ?
id: Completion.where(user_id: user_id).pluck(modyule_id)
)
end
PS: I turned it into multi line just for readability, you can change it back to a oneline if you like.

Django ORM Cross Product

I have three models:
class Customer(models.Model):
pass
class IssueType(models.Model):
pass
class IssueTypeConfigPerCustomer(models.Model):
customer=models.ForeignKey(Customer)
issue_type=models.ForeignKey(IssueType)
class Meta:
unique_together=[('customer', 'issue_type')]
How can I find all tuples of (custmer, issue_type) where there is no IssueTypeConfigPerCustomer object?
I want to avoid a loop in Python. A solution which solves this in the DB would be preferred.
Background: for every customer and for every issue-type, there should be a config in the DB.
If you can afford to make one database trip for each issue type, try something like this untested snippet:
def lacking_configs():
for issue_type in IssueType.objects.all():
for customer in Customer.objects.filter(
issuetypeconfigpercustomer__issue_type=None
):
yield customer, issue_type
missing = list(lacking_configs())
This is probably OK unless you have a lot of issue types or if you are doing this several times per second, but you may also consider having a sensible default instead of making a config object mandatory for each combination of issue type and customer (IMHO it is a bit of a design-smell).
[update]
I updated the question: I want to avoid a loop in Python. A solution which solves this in the DB would be preferred.
In Django, every Queryset is either a list of Model instances or a dict (values querysets), so it is impossible to return the format you want (a list of tuples of Model) without some Python (and possibly multiple trips to the database).
The closest thing to a cross product would be using the "extra" method without a where parameter, but it involves raw SQL and knowing the underlying table name for the other model:
missing = Customer.objects.extra(
select={"issue_type_id": 'appname_issuetype.id'},
tables=['appname_issuetype']
)
As a result, each Customer object will have an extra attribute, "issue_type_id", containing the id of one IssueType. You can use the where parameter to filter based on NOT EXISTS (SELECT 1 FROM appname_issuetypeconfigpercustomer WHERE issuetype_id=appname_issuetype.id AND customer_id=appname_customer.id). Using the values method you can have something close to what you want - this is probably enough information to verify the rule and create the missing records. If you need other fields from IssueType just include them in the select argument.
In order to assemble a list of (Customer, IssueType) you need something like:
cross_product = [
(customer, IssueType.objects.get(pk=customer.issue_type_id))
for customer in
Customer.objects.extra(
select={"issue_type_id": 'appname_issuetype.id'},
tables=['appname_issuetype'],
where=["""
NOT EXISTS (
SELECT 1
FROM appname_issuetypeconfigpercustomer
WHERE issuetype_id=appname_issuetype.id
AND customer_id=appname_customer.id
)
"""]
)
]
Not only this requires the same number of trips to the database as the "generator" based version but IMHO it is also less portable, less readable and violates DRY. I guess you can lower the number of database queries to a couple using something like this:
missing = Customer.objects.extra(
select={"issue_type_id": 'appname_issuetype.id'},
tables=['appname_issuetype'],
where=["""
NOT EXISTS (
SELECT 1
FROM appname_issuetypeconfigpercustomer
WHERE issuetype_id=appname_issuetype.id
AND customer_id=appname_customer.id
)
"""]
)
issue_list = dict(
(issue.id, issue)
for issue in
IssueType.objects.filter(
pk__in=set(m.issue_type_id for m in missing)
)
)
cross_product = [(c, issue_list[c.issue_type_id]) for c in missing]
Bottom line: in the best case you make two queries at the cost of legibility and portability. Having sensible defaults is probably a better design compared to mandatory config for each combination of Customer and IssueType.
This is all untested, sorry if some homework was left for you.

Rails Order by frequency of a column in another table

I have a table KmRelationship which associates Keywords and Movies
In keyword index I would like to list all keywords that appear most frequently in the KmRelationships table and only take(20)
.order doesn't seem to work no matter how I use it and where I put it and same for sort_by
It sounds relatively straight forward but i just can't seem to get it to work
Any ideas?
Assuming your KmRelationship table has keyword_id:
top_keywords = KmRelationship.select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20)
This may not look right in your console output, but that's because rails doesn't build out an object attribute for the calculated frequency column.
You can see the results like this:
top_keywords.each {|k| puts "#{k.keyword_id} : #{k.freqency}" }
To put this to good use, you can then map out your actual Keyword objects:
class Keyword < ActiveRecord::Base
# other stuff
def self.most_popular
KmRelationship.
select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20).
map(&:keyword)
end
end
And call with:
Keyword.most_popular
#posts = Post.select([:id, :title]).order("created_at desc").limit(6)
I have this listed in my controller index method which allows the the order to show the last post with a limit of 6. It might be something similar to what you are trying to do. This code actually reflects a most recent post on my home page.

How is the Arel#extract method used in rails?

I am trying to use the Arel#extract method. I have seen an example in a test case in test_extract.rb in the source but when I try to reproduce it in my app, I get undefined method.
table = Arel::Table.new :users
puts table[:created_at].extract('hour').to_sql
=> NoMethodError: undefined method `extract' for #<Arel::Attributes::Attribute:0x7..8>
I am using pg as the database.
Update:
My goal is to end up with this result in sql:
SELECT users.* FROM users WHERE EXTRACT(HOUR from users."created_at") = '1'
I would like to find all users that were created on the hour equal to one of any day. This works in sql but I am wondering how to create it in arel. Here is an example of how it's used in the arel test suite.
extract is node's method, you can cast it on any column(such as users[:id]), but not on Arel::Table instance.
So, to construct your query you need:
get Arel::Table instance users = Arel::Table.new(:users) or if you use ActiveRecord - users = User.arel_table
set SELECT statement on Arel::Table instance with project method: users = users.project(Arel.sql('*'))
set WHERE statement with where method: users.where(users[:created_at].extract(:hour).eq(1))
In one block:
query = User.arel_table.
project(Arel.sql('*')).
where(users[:created_at].extract(:hour).eq(1))
users = User.find_by_sql(query)
# => "SELECT * FROM \"users\" WHERE EXTRACT(HOUR FROM \"users\".\"created_at\") = 1"
I've had to perform an extract DOW on an instance of Arel::Nodes::NamedFunction, which does not expose the method #extract (as of Arel 6.0). I managed to achieve this by manually creating an instance of Arel::Nodes::Extract. Here is what worked for me, in case anyone have a similar issue:
Arel::Nodes::Extract.new(
Arel::Nodes::NamedFunction.new('some_function_name', [param1, param2, ...]),
:dow
)
You can use an Arel node directly with ActiveRecord's #where instead of building the full query through Arel as exemplified by Alexander Karmes's answer. So here is another way to perform the query required by the answer:
User.where(
Arel::Nodes::Extract.new(User.arel_table[:created_at], :hour).eq(1)
)
Which yields:
SELECT "users".* FROM "users" WHERE EXTRACT(HOUR FROM "users"."created_at") = 1
With the added benefit you can keep chaining other scopes defined in your User model.