5

I'm trying to run a query in a very quick and dirty way in Rails, without putting the rest of the model in place. I know this is bad practice but I just need a quick result in a tight timeframe until I've got the whole solution in place.

I've got items that have a shipping price, based on weight. The weight is stored in the item, the price is stored in the table shipping_zone_prices, and all I currently do is look for the price relating to the first row where the weight is heavier than the item for sale:

class Item < ActiveRecord::Base
  def shipping_price
    item_id = self.id
    shipping_price = ShippingZonePrice.find_by_sql(
      "SELECT z.price as price
       FROM shipping_zone_prices z, items i
       WHERE i.id = '#{item_id}'
       AND z.weight_g > d.weight
       ORDER BY z.weight_g asc limit 1")    
  end
end

This sort of works. The SQL does the job, but when plugged into the app as follows:

 <%= @item.shipping_price %> Shipping

I get the following displayed:

[#<ShippingZonePrice price: 12>] Shipping

In this example, '12' is the price that is being pulled from the db, and is correct. @item.shipping_price.class returns 'Array'. Trying to access the array using [0] (or any other integer) returns a blank.

Is there another way to access this, or am I missing something fundamental?

1
  • Well spotted, that should be i.weight - I was just trying to make the data a bit generic by using 'item' and I didn't make all the correct changes Commented Sep 19, 2012 at 15:09

5 Answers 5

7

Since you are defining an instance method, I think it should return the price if it exists or nil

Try something like this:

def shipping_price
  ShippingZonePrice.find_by_sql(
    "SELECT z.price as price
     FROM shipping_zone_prices z, items i
     WHERE i.id = '#{self.id}'
     AND z.weight_g > d.weight
     ORDER BY z.weight_g asc limit 1").first.try(:price)
end

Then this should work for you:

@item.shipping_price

The first.try(:price) part is needed because find_by_sql may return an empty array. If you tried to do something like first.price on an empty array, you would get an exception along the lines of NoMethodError: undefined method 'price' for nil:NilClass.

Sign up to request clarification or add additional context in comments.

Comments

5

This is because find_by_sql returns a model, not data. If you want to do a direct fetch of the data in question, use something like this:

ShippingZonePrice.connection.select_value(query)

There are a number of direct-access utility methods available through connection that can fetch single values, a singular array, rows of arrays, or rows of hashes. Look at the documentation for ActiveRecord::ConnectionAdapters::DatabaseStatements.

As when writing an SQL directly, you should be very careful to not create SQL injection bugs. This is why it is usually best to encapsulate this method somewhere safe. Example:

class ShippingZonePrice < ActiveRecord::Base
  def self.price_for_item(item)
    self.connection.select_value(
      self.sanitize_sql(
        %Q[
          SELECT z.price as price
            FROM shipping_zone_prices z, items i
            WHERE i.id=?
              AND z.weight_g > d.weight
            ORDER BY z.weight_g asc limit 1
        ],
        item.id
      )
    )
  end
end

Comments

3
@item.shipping_price.first.price

or

@item.shipping_price[0].price

Thanks Atastor for pointing that out!

When you use AS price in find_by_sql, price becomes a property of the result.

Comments

1

If not for you saying that you tried and failed accessing [0] i'ld say you want to put

@item.shipping_price.first.price # I guess BSeven just forgot the .first. in his solution

into the view...strange

Comments

1

So, I had a hacky solution for this, but it works great. Create a table that has the same output as your function and reference it, then just call a function that does a find_by_sql to populate the model.

Create a dummy table:

CREATE TABLE report.compliance_year (
 id BIGSERIAL,
 year TIMESTAMP,
 compliance NUMERIC(20,2),
 fund_id INT);

Then, create a model that uses the empty table:

class Visualization::ComplianceByYear < ActiveRecord::Base
    self.table_name = 'report.compliance_year'
    def compliance_by_year(fund_id)
        Visualization::ComplianceByYear.find_by_sql(["
            SELECT year, compliance, fund_id
              FROM report.usp_compliance_year(ARRAY[?])", fund_id])
    end
end 

In your controller, you can populate it:

def visualizations
  @compliancebyyear = Visualization::ComplianceByYear.new()
  @compliancefunds = @compliancebyyear.compliance_by_year(current_group.id)
  binding.pry
end

Then, you can see it populate with what you need:

[1] pry(#<Thing::ThingCustomController>)> @compliancefunds
[
[0] #<Visualization::ComplianceByYear:0x00000008f78458> {
          :year => Mon, 31 Dec 2012 19:00:00 EST -05:00,
    :compliance => 0.93,
       :fund_id => 1
},
[1] #<Visualization::ComplianceByYear:0x0000000a616a70> {
          :year => Tue, 31 Dec 2013 19:00:00 EST -05:00,
    :compliance => 0.93,
       :fund_id => 4129
},
[2] #<Visualization::ComplianceByYear:0x0000000a6162c8> {
          :year => Wed, 31 Dec 2014 19:00:00 EST -05:00,
    :compliance => 0.93,
       :fund_id => 4129
}
]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.