In Ruby on rails, our model includes orders and payments.
There's 1-many relationship between order and payments.
In the orders model we specify:
has_many :payments, :as => :payable
And a payment record has payable_id that is set to order.id.
In a report, I want to select all payments that belong to orders of a given type.
Using:
payments = Payment.find(:all, :conditions => conditions)
and adding 'payable.type="a" ' to the conditions doesn't work.
It seems that ActiveRecord doesn't develop this into a correct join statement (payable_id=order.id and orders.type='a').
I cannot use explicit SQL here, as the condition contains other things that are inserted there earlier in the code.
Thanks,
Raffi Lipkin
Your conditions clause is wrong.
You state that an Order
has_many :payments, :as => :payable
This tells me that a Payment
belongs_to :payable, :polymorphic => true
This means that the payments table has two columns of note: payable_id and payable_type. This also means that Payments can be applied not just to Orders, but also to other models as well (CreditCardBalances, who knows).
If you want to query for payments of a specific type, i.e. belonging to any instance of a particular class, you need to be querying the field payments.payable_type. This works fine:
Payment.find(:all, :conditions => "payable_type = 'Order'")
Here's a gist that shows what I did to test this. The models created are set up just like described above.
Don't forget that you can extract that into named scopes if it's easier:
named_scope :on_orders, :conditions => "payable_type = 'Order'"
Which makes it
Payment.on_orders
Or dynamically:
named_scope :on, lambda { |type| { :conditions => "payable_type = '#{type.to_s}'" } }
Which then makes it
Payment.on(Order) # or Payment.on(CreditCardBalance) or Payment.on("Order")
Try incliding and reference the actual table id name in the condition, rather than the association alias:
find(:include => "payments", :conditions => ["payment.type = ?", "x"]
You mention 'payment type'. If they're fairly static, have you considered using single table inheritance (STI) to subclass your different payment types? Then Rails will do all the magic to filter on type.
E.g.
class CreditCardPayment < Payment
...
end
It doesn't even need to exhibit different behaviour initially; however you'll probably find that it turns out to be really useful to have different data and polymorphic behaviour around payments.
Related
I am new to Ruby and ThinkingSphinx.
I have the following Sphinx Query - SELECT * FROM user_core, user_delta WHERE sphinx_deleted = 0.
I do not want to see the condition "WHERE 'sphinx_deleted' = 0. How do I remove this? I have removed the sql_attr_uint = sphinx_deleted from my sphinx.conf file, yet I see the sphinx_deleted being passed in the query.
Here is the index file definition:
ThinkingSphinx::Index.define :user, :with => :active_record, :delta => true do
indexes [first_name,last_name,display_name], :as=>:name, :sortable=>true
indexes first_name, :sortable => true
indexes last_name, :sortable => true
indexes display_name, :sortable => true
indexes email, :sortable => true
indexes phone, :sortable => true
indexes title, :sortable => true
has id, :as => :user_id
has roles(:id), :as => :role_ids
has jurisdictions(:id), :as => :jurisdiction_ids
set_property :delta => true
end
I do not have a sphinx_scope or default_sphinx_scope defined.
We are using thinking-sphinx-3.1.0 and ruby-2.1.0
The sphinx_deleted attribute is created by Thinking Sphinx, and is used in the following cases (using your scenario of a User model with core and delta indices in the examples):
When a User is deleted, sphinx_deleted is set to 1 for that record in both the core and delta indices - there's no point returning Sphinx records if the underlying ActiveRecord object no longer exists.
When a User is updated, the delta index is processed with the latest field and attribute details, and the core index's document has sphinx_deleted set to 1, so only the latest (accurate) information will match. e.g. if a user has their name changed from Fred to Georgina, a search for 'Fred' will not return Georgina, because the core index document (which does match) is filtered out.
That is why the attribute exists. You cannot tell Thinking Sphinx to not add it, nor can you remove that filter, short of mucking around in the internals of Thinking Sphinx.
If there is a specific reason for wanting to remove the attribute and filter, feel free to comment here, or you can open an issue on the GitHub repo, or post to the TS Google Group.
Update
Okay, further to this, there are three ways around it.
Option One:
The first way is to make the query to Sphinx yourself, using a Thinking Sphinx connection:
results = ThinkingSphinx::Connection.take do |connection|
connection.execute "SELECT * FROM user_core, user_delta"
end
Keep in mind that this returns raw Sphinx values, not ActiveRecord instances.
Option Two:
A more complicated alternative, though, is to have your own search middleware stack. First, you'll want to create a custom subclass of ThinkingSphinx::Middlewares::SphinxQL that removes the :sphinx_deleted filter:
class SphinxQLWithoutFilter < ThinkingSphinx::Middlewares::SphinxQL
def call(contexts)
contexts.each do |context|
Inner.new(context).call
end
app.call contexts
end
private
class Inner < ThinkingSphinx::Middlewares::SphinxQL::Inner
def inclusive_filters
super.except :sphinx_deleted
end
end
end
Then, create a new middleware stack which uses this new SphinxQL query middleware:
WithoutFilterMiddleware = ::Middleware::Builder.new do
use ThinkingSphinx::Middlewares::StaleIdFilter
use SphinxQLWithoutFilter
use ThinkingSphinx::Middlewares::Geographer
use ThinkingSphinx::Middlewares::Inquirer
use ThinkingSphinx::Middlewares::ActiveRecordTranslator
use ThinkingSphinx::Middlewares::StaleIdChecker
use ThinkingSphinx::Middlewares::Glazier
end
And then you can use that middleware stack in specific search queries:
User.search 'foo', :middleware => WithoutFilterMiddleware
It's worth noting the two middleware present in that stack for stale ids. They work together to catch any Sphinx results that do not have a matching ActiveRecord object, and re-run the Sphinx query up to three times filtering out those unmatched records. They're probably useful, but if you don't want to use them, you can remove them from your custom stack. However, without them, any Sphinx records that don't have matching ActiveRecord objects will be transformed into nils.
Option Three:
This is the more hackish version of the previous solution, but will apply to all searches, so probably isn't worthwhile: re-open the class that adds the filter with class_eval and change the method definition:
ThinkingSphinx::Middlewares::SphinxQL::Inner.class_eval do
def inclusive_filters
# normally:
# (options[:with] || {}).merge({:sphinx_deleted => false})
# but without the sphinx_deleted filter:
options[:with] || {}
end
end
Now, all that said: I presume you're not actually deleting users, but somehow the deletion callbacks are being fired anyway? Hence, users do exist but are currently being filtered out by Sphinx? If so, I highly recommend not using ActiveRecord's destroy method, and instead having a custom method to mark users as inactive. This avoids the callbacks, and thus avoids the need for any of the above 'solutions'.
Having this:
class Event < ActiveRecord::Base
belongs_to :historizable, :polymorphic => true
end
user = User.create!
I can:
Event.create!(:historizable => user)
But I can't:
Event.where(:historizable => user)
# Mysql2::Error: Unknown column 'events.historizable' in 'where clause'
I have to do this instead:
Event.where(:historizable_id => user.id, :historizable_type => user.class.name)
Update
Code that reproduces the issue: https://gist.github.com/fguillen/4732177#file-polymorphic_where_test-rb
This has been implemented in Rails master and will be available in
Rails 4. Thanks.
– #carlosantoniodasilva
I do this:
user.events
This is a proper AR query, you can chain it with other scopes and stuff:
user.events.where(<your event conditions here>)
EDIT: AFAIK the other way around you must specify both fields (makes sense: you could have a user with id 4 and another thing with events, like a Party, also with id 4).
EDIT2: Regarding "Why does create work and where doesn't": create is more highlevel than where, because the former deals with "a complete model", while the latter manages things at the database table level.
ActiveRecord's create (AFAIK) uses a combination of new + update_param somewhere down the line.
update_param uses your model's xxxx= methods for assigning each individual property.
In your example, historizable= is a method built by the belongs_to expression. Since the belongs_to "knows" that it's polymorphic, it can deal with the type and id.
On the other hand, when you pass a hash to the where clause, the parameters there only refer to database fields. Rails provides scopes for "higher level" access:
class Event < ActiveRecord::Base
...
scope :by_historizable, lambda { |h| where(:historizable_id => h.id, :historizable_type => h.class.name) }
end
...
Event.by_historizable(user).where(<your other queries here>)
I've heard that this might change in Rails 4, and where might be more "intelligent". But I have not checked yet.
Try:
Event.joins(:historizable).where(:historizable => {:historizable_type => user})
I'm trying to retrieve cars from my database where each car has a manufacturer, and can have multiple styles.
For example, a ford fiesta is a coupe, sedan and hatch.
I've got my relationships set-up in my models, but now I want to create a query to return the results. The query construction will depend on what parameters are supplied.
This is what I've got so far
conditions = {}
conditions[:manufacturer_id] = params[:manufacturer_id] unless params[:manufacturer_id].blank? # this works!
conditions[:style_id] = "style_id IN (?)", params[:style_ids] unless params[:style_ids].blank? #this breaks it :(
cars = Car.find(:all, :conditions=> conditions)
return render :json => cars
The error getting returned is
PG::Error: ERROR: column cars.style_ids does not exit of course this is because the style_id is in a join table called cars_styles. Is there a way to tell ActiveRecord which table to look for within the condition?
The key thing here is that I want to only have one controller method which takes the params in existence and then creates the right query. So if I don't have a manufacturer_id, it will only query the styles, or if vice versa. Of course, I'll be adding other params later too.
I ended up doing this with scoped queries like this
scope :from_manufacturer, lambda{|manu|{:joins => :manufacturer, :conditions => "manufacturers.id = #{manu}" }}
scope :from_style, lambda{|style|{:joins => :style, :conditions => "styles.id = #{style}"}}
def self.get_cars(params)
scope = self
[:manufacturer,:style].each do |s|
scope = scope.send("from_#{s}", params[s]) if params[s].present?
end
scope
end
Works great!
I have a model whose rows I always want to sort based on the values in another associated model and I was thinking that the way to implement this would be to use set_dataset in the model. This is causing query results to be returned as hashes rather than objects, though, so none of the methods from the class can be used when iterating over the dataset.
I basically have two classes
class SortFields < Sequel::Model(:sort_fields)
set_primary_key :objectid
end
class Items < Sequel::Model(:items)
set_primary_key :objectid
one_to_one :sort_fields, :class => SortFields, :key => :objectid
end
Some backstory: the data is imported from a legacy system into mysql. The values in sort_fields are calculated from multiple other associated tables (some one-to-many, some many-to-many) according to some complicated rules. The likely solution will be to just add the values in sort_fields to items (I want to keep the imported data separate from the calculated data, but I don't have to). First, though, I just want to understand how far you can go with a dataset and still get objects rather than hashes.
If I set the dataset to sort on a field in items like so
class Items < Sequel::Model(:items)
set_primary_key :objectid
one_to_one :sort_fields, :class => SortFields, :key => :objectid
set_dataset(order(:sortnumber))
end
then the expected clause is added to the generated SQL, e.g.:
>> Items.limit(1).sql
=> "SELECT * FROM `items` ORDER BY `sortnumber` LIMIT 1"
and queries still return objects:
>> Items.limit(1).first.class
=> Items
If I order it by the associated fields though...
class Items < Sequel::Model(:items)
set_primary_key :objectid
one_to_one :sort_fields, :class => SortFields, :key => :objectid
set_dataset(
eager_graph(:sort_fields).
order(:sort1, :sort2, :sort3)
)
end
...I get hashes
?> Items.limit(1).first.class
=> Hash
My first thought was that this happens because all fields from sort_fields are included in the results and maybe if selected only the fields from items I would get Items objects again:
class Items < Sequel::Model(:items)
set_primary_key :objectid
one_to_one :sort_fields, :class => SortFields, :key => :objectid
set_dataset(
eager_graph(:sort_fields).
select(:items.*).
order(:sort1, :sort2, :sort3)
)
end
The generated SQL is what I would expect:
>> Items.limit(1).sql
=> "SELECT `items`.* FROM `items` LEFT OUTER JOIN `sort_fields` ON (`sort_fields`.`objectid` = `items`.`objectid`) ORDER BY `sort1`, `sort2`, `sort3` LIMIT 1"
It returns the same rows as the set_dataset(order(:sortnumber)) version but it still doesn't work:
>> Items.limit(1).first.class
=> Hash
Before I add the sort fields to the items table so that they can all live happily in the same model, is there a way to tell Sequel to return on object when it wants to return a hash?
If you use #eager_graph, you must use #all instead of #each to retrieve the results in order for the graph to be processed (since you cannot eagerly load without having all instances up front), or use the eager_each plugin (which makes #each call #all internally).
I need to add status for an object, and need a hint about the Rails way to do this. Somewhere I've seen status was added into the model, but already lost where it was.
By status, I mean something that tracks about the item state. Like {0: :ORDERED, 1: :CHANGED, 2: :SHIPPED, 3: :ARCHIVED} for order in store. Looks like it needs id that stored in DB, constant or symbol that I could use in code instead of integer id, and one or two human readable messages for UI
There's a couple simple ways to do this. If the names of the statuses are short, I'd do basically what Samy suggested and store them directly in the model. So, in your migration, you'd do
add_column :orders, :status, :string
Then, in your model, you can use the status method to retrieve the status. You'll want to make sure you only store valid statuses, so you the :inclusion validator something like this:
class Order
validates :status, inclusion: { in: %w(ordered changed shipped archived) },
presence: true
end
If the statuses are longer, you can do something very much like the above with a short name for each status, then add an additional method to give you the full status message
class Order
STATUSES = { 'ordered' => 'Order placed',
'changed' => 'A change has been made to the order',
'shipped' => 'The order has been shipped',
'archived' => 'The order has been archived' }
def self.valid_statuses
STATUSES.keys
end
validates :status, inclusion: { in: valid_statuses },
presence: true
def extended_status
STATUSES[status]
end
end
If the problem has some complexity (f.e: lots of states, the object changes its behavior when changing its state...), you could use the gem StateMachine.
MagicFieldNames might be what you are looking for, it has a discriminator type column that you can use for Single Table Inheritance.
If you want simpler, you can use a status column which value can equal ordered, changed, or shipped. You don't even need to create constants in Rails or such a thing.