How do I sort a “foreign” column using datatable/will_paginate as in Railscast 340?

时光毁灭记忆、已成空白 提交于 2019-12-22 13:46:08

问题


Environment: Ruby 2.0.0, Rails 4.0.3, Windows 8.1, PostreSQL, Datatable 1.12.2, Will_Paginate 3.0.5

I’ve successfully implemented the Railscast 340 solution, thanks to help provided here. However, my table has columns that are not native to the displayed table. These columns are polymorphic relationships using has_many through.

The table being displayed is:

class Product < ActiveRecord::Base
  has_one :location, dependent: :destroy
  has_one :patron, through: :location, source: :locator, source_type: 'Patron'
  has_one :shelf, through: :location, source: :locator, source_type: 'Shelf'

EDIT to add the location (through) table:

class Location < ActiveRecord::Base
  belongs_to :product
  belongs_to :locator, polymorphic: true

One polymorphic column that needs to be used for sorting is:

class Shelf < ActiveRecord::Base
  has_many :locations, as: :locator, dependent: :nullify
  has_many :products, through: :locations, dependent: :nullify

class Patron < ActiveRecord::Base
  has_many :locations, as: :locator, dependent: :nullify
  has_many :products, through: :locations, dependent: :nullify

The column is being displayed normally. However, since the column doesn’t exist in the Product table, it is not available to sort using the basic statement:

products = Product.order("#{sort_column} #{sort_direction}")

I know that I could read the table, sort it and make it available for display, but that is what I am trying to avoid by implementing Railscast 340 due to the performance hit.

I assume there is some kind of query, join or sort sequence that would allow me to do this, but I am at a loss as to where to start… Reading the ActiveRecord Query Guide, though it is relatively good as far as guides go, leaves me more confused than when I started. Any direction as to how to attack this would be appreciated. Thanks.

EDIT Stack Trace Follows

Sort was:

products = Product.all.joins(:location).order("location.product.readable_loc #{sort_direction}")

Stack trace was:

Started GET "/products.json?sEcho=3&iColumns=8&sColumns=&iDisplayStart=0&iDisplayLength=10&mDataProp_0=0&mDataProp_1=1&mDataProp_2=2&mDataProp_3=3&mDataProp_4=4&mDataProp_5=5&mDataProp_6=6&mDataProp_7=7&sSearch=&bRegex=false&sSearch_0=&bRegex_0=false&bSearchable_0=true&sSearch_1=&bRegex_1=false&bSearchable_1=true&sSearch_2=&bRegex_2=false&bSearchable_2=true&sSearch_3=&bRegex_3=false&bSearchable_3=true&sSearch_4=&bRegex_4=false&bSearchable_4=true&sSearch_5=&bRegex_5=false&bSearchable_5=true&sSearch_6=&bRegex_6=false&bSearchable_6=true&sSearch_7=&bRegex_7=false&bSearchable_7=true&iSortCol_0=6&sSortDir_0=desc&iSortingCols=1&bSortable_0=true&bSortable_1=true&bSortable_2=true&bSortable_3=true&bSortable_4=true&bSortable_5=true&bSortable_6=true&bSortable_7=true&_=1401813038880" for 127.0.0.1 at 2014-06-03 14:50:55 -0400
Processing by ProductsController#index as JSON
  Parameters: {"sEcho"=>"3", "iColumns"=>"8", "sColumns"=>"", "iDisplayStart"=>"0", "iDisplayLength"=>"10", "mDataProp_0"=>"0", "mDataProp_1"=>"1", "mDataProp_2"=>"2", "mDataProp_3"=>"3", "mDataProp_4"=>"4", "mDataProp_5"=>"5", "mDataProp_6"=>"6", "mDataProp_7"=>"7", "sSearch"=>"", "bRegex"=>"false", "sSearch_0"=>"", "bRegex_0"=>"false", "bSearchable_0"=>"true", "sSearch_1"=>"", "bRegex_1"=>"false", "bSearchable_1"=>"true", "sSearch_2"=>"", "bRegex_2"=>"false", "bSearchable_2"=>"true", "sSearch_3"=>"", "bRegex_3"=>"false", "bSearchable_3"=>"true", "sSearch_4"=>"", "bRegex_4"=>"false", "bSearchable_4"=>"true", "sSearch_5"=>"", "bRegex_5"=>"false", "bSearchable_5"=>"true", "sSearch_6"=>"", "bRegex_6"=>"false", "bSearchable_6"=>"true", "sSearch_7"=>"", "bRegex_7"=>"false", "bSearchable_7"=>"true", "iSortCol_0"=>"6", "sSortDir_0"=>"desc", "iSortingCols"=>"1", "bSortable_0"=>"true", "bSortable_1"=>"true", "bSortable_2"=>"true", "bSortable_3"=>"true", "bSortable_4"=>"true", "bSortable_5"=>"true", "bSortable_6"=>"true", "bSortable_7"=>"true", "_"=>"1401813038880"}
  Company Load (1.0ms)  SELECT "companies".* FROM "companies" WHERE "companies"."prefix" = 'ucf' ORDER BY "companies"."id" ASC LIMIT 1
  Device Load (0.0ms)  SELECT "devices".* FROM "devices" WHERE "devices"."company_id" = 54 AND "devices"."id" = 601 ORDER BY "devices"."id" ASC LIMIT 1
   (1.0ms)  SELECT COUNT(*) FROM "roles" INNER JOIN "devices_roles" ON "roles"."id" = "devices_roles"."role_id" WHERE "devices_roles"."device_id" = $1 AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))  [["device_id", 601]]
   (0.0ms)  SELECT COUNT(*) FROM "products" WHERE "products"."company_id" = 54
   (1.0ms)  SELECT COUNT(*) FROM "products" INNER JOIN "locations" ON "locations"."product_id" = "products"."id" AND "locations"."company_id" = 54 WHERE "products"."company_id" = 54
  Product Load (2.0ms)  SELECT "products".* FROM "products" INNER JOIN "locations" ON "locations"."product_id" = "products"."id" AND "locations"."company_id" = 54 WHERE "products"."company_id" = 54 ORDER BY location.product.readable_loc desc LIMIT 10 OFFSET 0
PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "product"
LINE 1: ...id" = 54 WHERE "products"."company_id" = 54  ORDER BY product_locati...
                                                             ^
: SELECT  "products".* FROM "products" INNER JOIN "locations" ON "locations"."product_id" = "products"."id" AND "locations"."company_id" = 54 WHERE "products"."company_id" = 54  ORDER BY location.product.readable_loc desc LIMIT 10 OFFSET 0
Completed 500 Internal Server Error in 22ms

PG::UndefinedTable - ERROR:  missing FROM-clause entry for table "product"
LINE 1: ...id" = 54 WHERE "products"."company_id" = 54  ORDER BY product_locati...
                                                             ^
:
  activerecord (4.0.3) lib/active_record/connection_adapters/postgresql_adapter.rb:774:in `exec_no_cache'
  activerecord (4.0.3) lib/active_record/connection_adapters/postgresql/database_statements.rb:138:in `block in exec_query'
  activerecord (4.0.3) lib/active_record/connection_adapters/abstract_adapter.rb:435:in `block in log'
  activesupport (4.0.3) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
  activerecord (4.0.3) lib/active_record/connection_adapters/abstract_adapter.rb:430:in `log'
  activerecord (4.0.3) lib/active_record/connection_adapters/postgresql/database_statements.rb:137:in `exec_query'
  activerecord (4.0.3) lib/active_record/connection_adapters/postgresql_adapter.rb:891:in `select'
  activerecord (4.0.3) lib/active_record/connection_adapters/abstract/database_statements.rb:24:in `select_all'
  activerecord (4.0.3) lib/active_record/connection_adapters/abstract/query_cache.rb:61:in `block in select_all'
  activerecord (4.0.3) lib/active_record/connection_adapters/abstract/query_cache.rb:76:in `cache_sql'
  activerecord (4.0.3) lib/active_record/connection_adapters/abstract/query_cache.rb:61:in `select_all'
  activerecord (4.0.3) lib/active_record/querying.rb:36:in `find_by_sql'
  activerecord (4.0.3) lib/active_record/relation.rb:585:in `exec_queries'
  activerecord (4.0.3) lib/active_record/relation.rb:471:in `load'
  activerecord (4.0.3) lib/active_record/relation.rb:220:in `to_a'
  will_paginate (3.0.5) lib/will_paginate/active_record.rb:124:in `to_a'
  activerecord (4.0.3) lib/active_record/relation.rb:598:in `exec_queries'
  activerecord (4.0.3) lib/active_record/relation.rb:471:in `load'
  activerecord (4.0.3) lib/active_record/relation.rb:220:in `to_a'
  will_paginate (3.0.5) lib/will_paginate/active_record.rb:127:in `block in to_a'
  will_paginate (3.0.5) lib/will_paginate/collection.rb:96:in `create'
  will_paginate (3.0.5) lib/will_paginate/active_record.rb:126:in `to_a'
  D:65535:in `map'
  app/datatables/products_datatable.rb:20:in `data'
  app/datatables/products_datatable.rb:13:in `as_json'
  activesupport (4.0.3) lib/active_support/json/encoding.rb:50:in `block in encode'
  activesupport (4.0.3) lib/active_support/json/encoding.rb:81:in `check_for_circular_references'
  activesupport (4.0.3) lib/active_support/json/encoding.rb:49:in `encode'
  activesupport (4.0.3) lib/active_support/json/encoding.rb:34:in `encode'
  activesupport (4.0.3) lib/active_support/core_ext/object/to_json.rb:16:in `to_json'
  actionpack (4.0.3) lib/action_controller/metal/renderers.rb:90:in `block in <module:Renderers>'
  actionpack (4.0.3) lib/action_controller/metal/renderers.rb:33:in `block in _handle_render_options'
  D:/BitNami/rubystack-2.0.0-11/ruby/lib/ruby/2.0.0/set.rb:232:in `each'
  actionpack (4.0.3) lib/action_controller/metal/renderers.rb:30:in `_handle_render_options'
  actionpack (4.0.3) lib/action_controller/metal/renderers.rb:26:in `render_to_body'
  actionpack (4.0.3) lib/abstract_controller/rendering.rb:97:in `render'
  actionpack (4.0.3) lib/action_controller/metal/rendering.rb:16:in `render'
  actionpack (4.0.3) lib/action_controller/metal/instrumentation.rb:41:in `block (2 levels) in render'
  activesupport (4.0.3) lib/active_support/core_ext/benchmark.rb:12:in `block in ms'
  D:/BitNami/rubystack-2.0.0-11/ruby/lib/ruby/2.0.0/benchmark.rb:296:in `realtime'
  activesupport (4.0.3) lib/active_support/core_ext/benchmark.rb:12:in `ms'
  actionpack (4.0.3) lib/action_controller/metal/instrumentation.rb:41:in `block in render'
  actionpack (4.0.3) lib/action_controller/metal/instrumentation.rb:84:in `cleanup_view_runtime'
  activerecord (4.0.3) lib/active_record/railties/controller_runtime.rb:25:in `cleanup_view_runtime'
  actionpack (4.0.3) lib/action_controller/metal/instrumentation.rb:40:in `render'
  app/controllers/products_controller.rb:8:in `block (2 levels) in index'
  actionpack (4.0.3) lib/action_controller/metal/mime_responds.rb:191:in `respond_to'
  app/controllers/products_controller.rb:6:in `index'
  actionpack (4.0.3) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
  actionpack (4.0.3) lib/abstract_controller/base.rb:189:in `process_action'
  actionpack (4.0.3) lib/action_controller/metal/rendering.rb:10:in `process_action'
  actionpack (4.0.3) lib/abstract_controller/callbacks.rb:18:in `block in process_action'
  activesupport (4.0.3) lib/active_support/callbacks.rb:453:in `_run__936629966__process_action__callbacks'
  activesupport (4.0.3) lib/active_support/callbacks.rb:80:in `run_callbacks'
  actionpack (4.0.3) lib/abstract_controller/callbacks.rb:17:in `process_action'
  actionpack (4.0.3) lib/action_controller/metal/rescue.rb:29:in `process_action'
  actionpack (4.0.3) lib/action_controller/metal/instrumentation.rb:31:in `block in process_action'
  activesupport (4.0.3) lib/active_support/notifications.rb:159:in `block in instrument'
  activesupport (4.0.3) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
  activesupport (4.0.3) lib/active_support/notifications.rb:159:in `instrument'
  actionpack (4.0.3) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
  actionpack (4.0.3) lib/action_controller/metal/params_wrapper.rb:245:in `process_action'
  activerecord (4.0.3) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
  actionpack (4.0.3) lib/abstract_controller/base.rb:136:in `process'
  actionpack (4.0.3) lib/abstract_controller/rendering.rb:44:in `process'
  actionpack (4.0.3) lib/action_controller/metal.rb:195:in `dispatch'
  actionpack (4.0.3) lib/action_controller/metal/rack_delegation.rb:13:in `dispatch'
  actionpack (4.0.3) lib/action_controller/metal.rb:231:in `block in action'
  actionpack (4.0.3) lib/action_dispatch/routing/route_set.rb:80:in `dispatch'
  actionpack (4.0.3) lib/action_dispatch/routing/route_set.rb:48:in `call'
  actionpack (4.0.3) lib/action_dispatch/journey/router.rb:71:in `block in call'
  actionpack (4.0.3) lib/action_dispatch/journey/router.rb:59:in `call'
  actionpack (4.0.3) lib/action_dispatch/routing/route_set.rb:680:in `call'
  request_store (1.0.5) lib/request_store/middleware.rb:9:in `call'
  warden (1.2.3) lib/warden/manager.rb:35:in `block in call'
  warden (1.2.3) lib/warden/manager.rb:34:in `call'
  rack (1.5.2) lib/rack/etag.rb:23:in `call'
  rack (1.5.2) lib/rack/conditionalget.rb:25:in `call'
  rack (1.5.2) lib/rack/head.rb:11:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/params_parser.rb:27:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/flash.rb:241:in `call'
  rack (1.5.2) lib/rack/session/abstract/id.rb:225:in `context'
  rack (1.5.2) lib/rack/session/abstract/id.rb:220:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/cookies.rb:486:in `call'
  activerecord (4.0.3) lib/active_record/query_cache.rb:36:in `call'
  activerecord (4.0.3) lib/active_record/connection_adapters/abstract/connection_pool.rb:626:in `call'
  activerecord (4.0.3) lib/active_record/migration.rb:369:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/callbacks.rb:29:in `block in call'
  activesupport (4.0.3) lib/active_support/callbacks.rb:373:in `_run__103024161__call__callbacks'
  activesupport (4.0.3) lib/active_support/callbacks.rb:80:in `run_callbacks'
  actionpack (4.0.3) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/reloader.rb:64:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/remote_ip.rb:76:in `call'
  better_errors (1.1.0) lib/better_errors/middleware.rb:84:in `protected_app_call'
  better_errors (1.1.0) lib/better_errors/middleware.rb:79:in `better_errors_call'
  better_errors (1.1.0) lib/better_errors/middleware.rb:56:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/debug_exceptions.rb:17:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
  railties (4.0.3) lib/rails/rack/logger.rb:38:in `call_app'
  railties (4.0.3) lib/rails/rack/logger.rb:20:in `block in call'
  activesupport (4.0.3) lib/active_support/tagged_logging.rb:67:in `block in tagged'
  activesupport (4.0.3) lib/active_support/tagged_logging.rb:25:in `tagged'
  activesupport (4.0.3) lib/active_support/tagged_logging.rb:67:in `tagged'
  railties (4.0.3) lib/rails/rack/logger.rb:20:in `call'
  quiet_assets (1.0.2) lib/quiet_assets.rb:18:in `call_with_quiet_assets'
  actionpack (4.0.3) lib/action_dispatch/middleware/request_id.rb:21:in `call'
  rack (1.5.2) lib/rack/methodoverride.rb:21:in `call'
  rack (1.5.2) lib/rack/runtime.rb:17:in `call'
  activesupport (4.0.3) lib/active_support/cache/strategy/local_cache.rb:83:in `call'
  rack (1.5.2) lib/rack/lock.rb:17:in `call'
  actionpack (4.0.3) lib/action_dispatch/middleware/static.rb:64:in `call'
  rack (1.5.2) lib/rack/sendfile.rb:112:in `call'
  railties (4.0.3) lib/rails/engine.rb:511:in `call'
  railties (4.0.3) lib/rails/application.rb:97:in `call'
  rack (1.5.2) lib/rack/content_length.rb:14:in `call'
  thin (1.6.2) lib/thin/connection.rb:86:in `block in pre_process'
  thin (1.6.2) lib/thin/connection.rb:84:in `pre_process'
  thin (1.6.2) lib/thin/connection.rb:53:in `process'
  thin (1.6.2) lib/thin/connection.rb:39:in `receive_data'
  eventmachine-1.0.3-x86 (mingw32) lib/eventmachine.rb:187:in `run'
  thin (1.6.2) lib/thin/backends/base.rb:73:in `start'
  thin (1.6.2) lib/thin/server.rb:162:in `start'
  rack (1.5.2) lib/rack/handler/thin.rb:16:in `run'
  rack (1.5.2) lib/rack/server.rb:264:in `start'
  railties (4.0.3) lib/rails/commands/server.rb:84:in `start'
  railties (4.0.3) lib/rails/commands.rb:76:in `block in <top (required)>'
  railties (4.0.3) lib/rails/commands.rb:71:in `<top (required)>'
  bin/rails:4:in `<top (required)>'
  ruby-debug-ide (0.4.23.beta1) lib/ruby-debug-ide.rb:86:in `debug_program'
  ruby-debug-ide (0.4.23.beta1) bin/rdebug-ide:110:in `<top (required)>'
  -e:1:in `<main>'

EDIT Adding Product method readable_loc

  def readable_loc
    if self.location.locator.class == Patron
      self.location.locator.name  # Yields patron's name
    else
      self.location.locator.row.name + " " + self.location.locator.name  # Yields row name and shelf name. 
    end
  end

回答1:


I think you should use it like this:

products = Product.joins(:location).order("locations.#{sort_column} #{sort_direction}")

This will generate much nicer SQL query than .includes version, but I am not sure how it works with has_one association.




回答2:


products = Product.all.joins(:location).order("location.product.readable_loc {sort_direction}")

This won't work, as you discovered. The argument to order must be SQL snippet, you can't call instance methods, associations, etc. from the model.

So you need to use the value of a column, or write custom SQL to compute the order clause. This is made even more complicated in your example because of the polymorphic joins; we don't know how to order the result set without first digging through several associations. I don't see a way to do this without explicitly writing the joins and the order clause; see below for a possible solution:

  scope :by_readable_loc, -> { joins(join_clause).order("#{ order_clause } #{sort_direction}") }

  def self.join_clause
    <<-EOS
    JOIN "locations" ON "locations"."product_id" = "products"."id"
    LEFT JOIN "patrons" ON "locations"."locator_id" = "patrons"."id" AND "locations"."locator_type" = 'Patron'
    LEFT JOIN "shelves" ON "locations"."locator_id" = "shelves"."id" AND "locations"."locator_type" = 'Shelf'
    LEFT JOIN "rows" ON "shelves"."row_id" = "rows"."id"
    EOS
  end

  def self.order_clause
    <<-EOS
    CASE WHEN locations.locator_type = 'Patron' THEN "patrons"."name"
    ELSE "rows"."name" || ' ' || "shelves"."name"
    END
    EOS
  end

This implements readable_loc as a class method called order_clause, which can be passed into a scope, e.g. Product.by_readable_loc. You'd need to add LIMIT and OFFSET as appropriate for pagination.




回答3:


I ended up using a bit of a hack to solve this problem. Database gurus may want to look away...

The problem is more complex than originally stated. Any non-native column was not sorting correctly, whether or not it was polymorphic. It was sorting by the foreign key, not by the value of the foreign column being displayed in the table.

For every foreign column, I created a local shadow column within the original table that holds the desired value. The sort_column uses that shadow column to sort the table, but that is the only use for the shadow column. The datatable continues to reference the foreign table through the foreign key for all values even for display.

This did create the quandary as to how to make sure the shadow column was maintained in synchronization with the foreign table. I am using the after_update callback to do this. If any column is out of synchronization, all shadow columns are updated and the record is saved. For me, this occurs even when the record is created due to its associations.

I had initially tried to do it directly with before_save, but the associations were causing problems because they could not be accessed before the record was saved without the record being saved. You could see that would be an issue. In any case, this works for me. YMMV, so be careful out there.

after_update :shadow_update

... 

def shadow_update
  # After record is updated, update shadow columns if needed and force an update to write them
  # This actually triggers after create as well, but with correct timing regarding associations
  # Since all columns are updated to resolve differences, one update should resolve all
  if self.locshadow != self.location or self.year != self.yrshadow or ...
    self.locshadow = self.location
    self.yrshadow = self.year
    ...
    self.save!
  end
end



回答4:


You should use http://apidock.com/rails/ActiveRecord/QueryMethods/includes and direct table's name in order method

products = Product.includes(:location).order("locations.#{sort_column} #{sort_direction}")


来源:https://stackoverflow.com/questions/24001705/how-do-i-sort-a-foreign-column-using-datatable-will-paginate-as-in-railscast-3

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!