How Can I refactor this DB Query in Rails - ruby-on-rails

Disclaimer: I'm new to Rails.
What I'm trying to do is this in the long form:
user = Spree::User.find(2)
cart = Spree::Order.where(state: "cart", user_id: user.id)
line_item = Spree::LineItem.where(order_id: cart.last.id).map { |order| order.variant_id}
variant = Spree::Variant.find(line_item).map { |order| order.product_id }
Spree::Product.find(variant).map { |product| product.name }
What happens is it goes into the DB, finds the user, gets an order where their state is in cart. Then goes and finds its line item. Goes over to find the "variant" data to find out what products are included. Then over to the products page to tell me all products in a users cart.
This looks ugly and garjumbled. You wouldn't happen to know a way to refactor it and not do so much querying?

If you look at Spree Order States, it's not that you have multiple orders in different states, just one order that could be in one particular state.
It really depends on Spree::User. I can't find documentation and/or source for Spree::User, but mentions to a certain LegacyUser.
Since in the comments you mentioned you have a custom build, you need to check the actual code for Spree::User and see if a user can have one or many orders, and if there's already a method for selecting cart state orders.
So if Spree::User has one order:
order = user.order
order.variants.map(&:name)
Else:
order = user.orders.where(state: "cart")
order.variants.map(&:name)
There could be a method to get the order you need from the user in the code.

Related

.order("RANDOM()") with will_paginate gem

I am wondering if there is any way to still use the .order("RANDOM()") with will_paginate so that when a page loads and it orders the pages, all the post will stay the same on each page until the home page is reloaded.
so to have all the posts on localhost:3000/posts?page=1 stay the same until localhost:3000(root_path) is visited again.
Problem is it will paginate posts but it current re orders them for each page selected so you will often see posts on page 1 also on page 2.
One way to do this is to set the random seed which your database is ordering by, such that it returns the same sequence of random numbers each time. You can store this seed in your users' session, and reset it only when you want to. However, there's a complication -- even though setting the random seed produces the same ordering of random numbers each time, there's no guarantee your database will execute it on your rows in the same order each time, unless you force it to do so like so:
SELECT items.*
FROM (SELECT setseed(0.2)) t
, (SELECT name, rank() OVER (ORDER BY name DESC)
FROM foos ORDER BY name DESC) items
JOIN generate_series(1, (SELECT COUNT(*) FROM foos))
ON items.rank = generate_series
ORDER BY RANDOM()
LIMIT 10;
As you can tell, that's quite complicated, and it forces your database to materialize your entire table into memory. It'd work for smaller data sets, but if you've got a big data set, it's out of the question!
Instead, I'd suggest you go with a solution more like tadman suggested above: generate a page of results, store the ids into session, and when you need to generate the next page, simply ignore anything you've already shown the user. The code would look like:
class ThingsController < ApplicationController
def index
#page = params[:page].to_i
session[:pages] ||= {}
if ids = session[:pages][#page]
# Grab the items we already showed, and ensure they show up in the same order.
#things = Things.where(id: ids).sort_by { |thing| ids.index(thing.id) }
else
# Generate a new page of things, filtering anything we've already shown.
#things = Things.where(["id NOT IN (?)", shown_thing_ids])
.order("RANDOM()")
.limit(30) # your page size
# Save the IDs into our session so the above case will work.
session[:pages][#page] = #things.map(&:id)
end
end
private
def shown_thing_ids
session[:pages].values.flatten
end
end
This method uses the session to store which IDs were shown on each page, so you can guarantee the same set of items and ordering will be shown if the user goes back. For a new page, it will exclude any items already displayed. You can reset the cache whenever you want with:
session.delete(:pages)
Hope that helps! You could also use Redis or Memcache to store your page data, but the session is a good choice if you want the ordering to be random per-user.

Creating Rails todo list app with goals and tasks on one page - missing params

I'm a Rails beginner, and have been reading tutorials and typing out applications for a few months now. I'm really enjoying it after a few years spent in the front end world, and beginning to get up to speed with it all. The time has come though for me to start building my own stuff without any handholding. So far, so good.
I'm creating a basic to-do list app, where goals and tasks are displayed on the same page - goals#index. My issue is that I'm not sure how to get all tasks for a particular goal (that belongs to a user). I understand that I need to pass an ID param to the Goal model in order to find out its tasks, like so:
Goal.find(1).tasks
The above works fine, as I've already set up foreign keys on the tasks table and have a has_many :tasks relationship for the Goal model and a belongs_to relationship for the Task model.
Here's my Goals controller:
def index
#user = current_user
#goals = #user.goals # list all goals for the current user and assign it to the #goals variable.
# Need to find all tasks for each goal ID and assign it to the #tasks variable. Goal ID needs to be supplied here, but it isn't as we're not in show action.
#tasks = Goal.find(1).tasks
As I said, I can find all tasks for a Goal when I manually enter the ID (1 in this example). This works fine in my app, no errors. But obviously I want to supply these IDs dynamically, and I'm just not sure how I get the params in there.
I have tried the below:
#tasks = Goal.find(params[:id]).tasks
and
#tasks = Goal.find(params[:goal_id]).tasks
And I get the "Couldn't find Goal without an ID" error when I try to iterate over #tasks in my view. Which makes sense, as I don't think the goal params are being passed to it as we're not in the Show action.
Surely there must be an easy Rails way?! I'm stumped and don't really know where to look. Thanks for your help and Happy New Year.
You are getting current user's goals, so when you will do this you have one array object. so when you will pass array object to find, it will have multiple IDS. so when need to find All the tasks from all goals you just need to pass Array of IDS instead of single value.
#tasks = Task.where(:goal_id => #goals)
This will run this SQL query.
SELECT "tasks".* FROM "tasks" WHERE "tasks"."goal_id" IN (SELECT
"goals"."id" FROM "goals")
So when you are dealing with array just pass ids. for e.g. [1,2,3]
Once you do #goals = #user.goals (assuming that's working, which it sounds like it is), you have your goals and there is no reason to go back to the DB to "find" them.
To get ALL your tasks from ALL of user's goals, you can do the following:
#tasks = []
#goals.each do |goal|
#tasks << goal.tasks
end
Ah of course, #goals is an array of the user's goals so I can just work with that. So simple when someone just tells you. Thanks for all your help!
Here's my final code that works (I left the controller unchanged). This gets the first goal in the array and then gets the tasks associated with each goal. I have a set number of goals so I can just use goals[0], goals[1] or goals[2] for each goal.
<% #goals[0].tasks.each do |task| %>
<li><div class="task-item"><%= task.task_name %></div></li>
<% end %>

Enable random access to collection with MongoDB

I have a Rails application. It has a feed that shows items from different users all mixed up. It would be something similar to Pinterest in the way you see these items.
Right now I show all these items ordered by its date of creation. However, as the items are created by batches by users, they are shown not randomly (say you se the first 6 items being from one user, then the other 5 from other one, etc.).
The code that serves the items is this:
class Feeder
def self.most_recent_created(watching_user=nil, current_cursor)
next_cursor = nil
feed = []
influencers_ids = User.influencers.distinct(:_id)
Rating.most_recent_from_influencers(watching_user, influencers_ids).scroll(current_cursor) do |rating, cursor|
next_cursor = cursor
feed << ImoPresenter.new(Imo.new(rating), watching_user)
end
feed << next_cursor.to_s
end
end
scroll just gives a cursor pointing to each item in the iteration. Then I push the item into the feed.
The access to the database is done in Rating.most_recent_from_influencers(watching_user, influencers_ids), where most_recent_from_influencers(watching_user, influencers_ids) is a scope defined as follows:
scope :not_from, ->(user) { ne(user_id: user.id) }
scope :from, ->(user_ids) { any_in(user_id: user_ids) }
scope :most_recent_from_influencers, ->(watching_user, influencers_ids) {
proxy = from(influencers_ids).over_zero.desc(:created_at).limit(IMOS_PER_PAGE)
proxy = proxy.not_from(watching_user) if watching_user
proxy
}
MongoDB does not have random access out of the box. They suggest this for having a way of accessing randomly to the items. Basically, the solution is to add a random field in all documents and order the collection through this field. However, although I would have random items, I would always have almost the same items being shown, as I would just have the options of ordering it by desc(:rand) or asc(:rand).
I would like to have suggestions on how I can make the items being shown truly in a random way. Is it possible?
Based on similar questions, I've come to the conclusion that having a random field is a valid solution if the collection is dynamic, meaning that documents are inserted frequently. The more dynamic the collection is, the more 'random' access you can have.

Nil Values When Iterating Through Arrays and Some Really Weird Results

I'm a rails newbie and am building an app. My current problem is trying to find the average time since the last purchase for each customer of an online store using the app, where we have data about their orders and their customers. The problem is that I'm getting an error that says "undefined method `src_created_at' for nil:NilClass." Right now I'm trying to do this only for customers that have purchased once, and leaving aside those that purchased multiple times.
Here's my code:
#customers = Customer.where(:identity_id => #identity.id)
#single_order_customers = #customers.where("orders_count = ?", 1)
days_since_array = []
#single_order_customers.each do |s|
one_order = Order.where(:identity_id => #identity.id, :src_customer_id => s.src_id)
the_date = one_order[0].src_created_at
purchase_date = the_date.to_date
days_between = (Date.today - purchase_date)
days_since_array << days_between
end
days = days_since_array.inject(:+)
#adslp = days / days_since_array.count
Thanks in advance. I can provide what customer and order data looks like if necessary. Any advice would help, even though I know this question is somewhat vague. I've tried some kind if and unless statements validating presence or nil values and they're not working.
Edit:
Here's what's run in the console:
Order Load (123.0ms) SELECT "orders".* FROM "orders" WHERE "orders"."identity_id" = 2 AND "orders"."src_customer_id" = '114863554'
NoMethodError: undefined method `src_created_at' for nil:NilClass
(The above is several orders successfully run and then breaking on the last I've shown.)
Last point: when I try, specifically, to find nil values for this purpose I don't find any.
Order.where(:identity_id => 2, :src_created_at => nil)
Order Load (209.6ms) SELECT "orders".* FROM "orders" WHERE "orders"."identity_id" = 2 AND "orders"."src_created_at" IS NULL
=> []
For your last point where you tried to find nil values, you are getting an empty array [] because your query returned an empty set (no records matched your query). Trying to access any index on an empty array in rails won't throw an IndexOutOfBoundsException, but will instead just return nil, which is where you are getting your nil from. You are expecting one_order[0] to be an order item, but it is instead nil.
The solution to your problem would be to make sure the Order you are searching for already exists in the database. You might want to check how your orders are being created, e.g. if you use Order.create(params[:order]), do you have any validations that are failing, etc. You can check the orders you have through the rails console; just run Order.all. There should be an Order that has an identity_id of 2 and src_created_at of nil for the last query you wrote to return an actual order in the set.
A quick fix for now would be to remove the extra query to set one_order and just get it from the customer:
...
#single_order_customers.each do |s|
one_order = s.orders.first
the_date = one_order.src_created_at
...
The extra query does not seem to be necessary. You have already filtered #customers by identity_id, and src_customer_id will definitely match s.src_id if you get orders by calling s.orders. Relationships should be set up correctly in the models so that a customer has_many orders. You mentioned that you are just starting with rails; I would highly recommend reading a tutorial for rails first to save yourself headaches like these in the future :)

Querying the cache in rails still runs a call against the DB

I'm probably missing something very simple here, but can't understand what.
I'm trying to cache a simple active record query but every time I touch the cache, it runs the query against the DB again.
Controller Code:
products = Rails.cache.read("search_results")
if products
render :text => products[0].id
else
products = Product.where('name LIKE ?", 'product_name?')
Rails.cache.write("search_results", products)
end
I can see that in my second call I get to the if block and not the else, but any time I'm trying to touch products (like rendering it) I also see an active record call to the DB.
What am I missing?
The line
products = Product.where('name LIKE ?", 'product_name?')
returns an ActiveRecord::Relation, but does not hit the database unless a kicker method is called on it.
While I would still recommend using fetch as mentioned in my comment above, try changing the line to:
products = Product.where('name LIKE ?", 'product_name?').all
which will force the database hit, and save the actual results of the query into the cache, instead of the relation.

Resources