jbox-web / ajax-datatables-rails

A wrapper around DataTable's ajax methods that allow synchronization with server-side pagination in a Rails app
MIT License
585 stars 228 forks source link

Polymorphic Associations #347

Closed aximuseng closed 4 years ago

aximuseng commented 4 years ago

The documentation and issues do not clearly outline how (if possible at all) to use polymorphic associations.

I have a simple History model:

class History < ActiveRecord::Base
  belongs_to :historical, polymorphic: true, touch: true
  belongs_to :user
end

Just a basic way to keep track of changes etc.

Here is my datatable file:

class HistoryDatatable < ApplicationDatatable

  def_delegators :@view, :content_tag, :link_to, :policy, :policy_scope, :action_name, :poly_parent, :controller_name, :distance_of_time

  def view_columns
    @view_columns ||= {
      type:     { source: 'History.historical_type'},
      historical_name:     { source: 'Historical.name'},
      parent_location:    { source: 'Historical.parent_location'},
      action: { source: 'History.action'},
      updated_at:     { source: 'History.updated_at'},
      user:     { source: 'User.name_full'},
      note:    { source: 'History.note'}
    }
  end

  def sortable_columns
    @sortable_columns ||= [
      'Historical.name',
      'Historical.parent_location',
      'User.name_full'
    ]
  end

  def get_raw_records
#    History.includes(:user, :historical).references(:user)
    History.joins(:user).includes(:historical)
  end

  def data
    records.map do |history|
      {
        type:               history.historical_type.underscore.humanize.titlecase,
        historical_name:    link_to(history.historical.name, history.historical),
        parent_location:    !history.historical.parent_location.present? ? "None" : link_to(history.historical.parent_location.name, history.historical.parent_location),
        action:             history.action.titlecase,
        updated_at:         distance_of_time(history.updated_at),
        user:               link_to(history.user.name_full, history.user), 
        note:               history.note,
        DT_RowId:           history.id
      }
    end
  end

end

My UI works fine EXCEPT when trying to search or sort on associations (even the User association). My logs always show a variation of (Sorting on Historical columns or regular Search):

{"method":"GET","path":"/history/datatable.json","format":"json","controller":"HistoriesController","action":"datatable","status":500,"error":"NameError: uninitialized constant Historical","duration":15.75,"view":0.0,"db":3.23}

NameError - uninitialized constant Historical:
  app/datatables/history_datatable.rb:31:in `data'
  app/controllers/histories_controller.rb:49:in `datatable'

And sorting on the User column:

{"method":"GET","path":"/history/datatable.json","format":"json","controller":"HistoriesController","action":"datatable","status":500,"error":"ActiveRecord::EagerLoadPolymorphicError: Cannot eagerly load the polymorphic association :historical","duration":17.07,"view":0.0,"db":4.57}

ActiveRecord::EagerLoadPolymorphicError - Cannot eagerly load the polymorphic association :historical:
  app/datatables/history_datatable.rb:31:in `data'
  app/controllers/histories_controller.rb:49:in `datatable'
toomanyjoes commented 4 years ago

This all seems fine except that in your view columns and sortable columns you want to use the actual table.column_name thats in your database. Not the active record model name. When mapping your records you can use the active record model name and model functions, however you cannot sort/search on model function since all sorting and searching is doing building a SQL query that executes against your database, so if a column doesn't actually exist it can't be sorted or searched.

Your last issue doesn't seem to be caused by any code you've provided, you must have some code somewhere that is trying to eager load your active record association. Does your controller or view do anything with that relation? Call a "count" function or something that would eagerly load it? That would be my hunch.

aximuseng commented 4 years ago

I think this is where this hits a dead end. Historical is the polymorphic association. Historical.name is actually Model1.name, Model2.name etc.

There is no other DB calls in the controller and view. These errors are triggered in the datatable controller when I try to sort on the polymorphic related fields.

I hate storing redundant data but I think I may just create a few more fields and then store these values so that a simple search is easier to perform.

I also may be be able to create a complex multiple JOIN WHERE and then search those related fields.

toomanyjoes commented 4 years ago

I don't know your use case entirely so this may not help, but I was able to use the polymorphic association by its name through an actual model. So if theres something linking historical through the history model I think you should be able to get away with

def view_columns
    @view_columns ||= {
      type:     { source: 'History.historical_type'},
      historical_name:     { source: 'History.historical.name'},
      parent_location:    { source: 'History.historical.parent_location'},
      action: { source: 'History.action'},
      updated_at:     { source: 'History.updated_at'},
      user:     { source: 'User.name_full'},
      note:    { source: 'History.note'}
    }
  end

That may at least quiet the errors. Again I don't think you will be able to sort/search on these columns. I can't say for sure because all the fields I was using from my polymorphic association were also model functions and not actual DB columns.

aximuseng commented 4 years ago

Yup that's what I figured the more I dug into it.

ajahongir commented 4 years ago

@toomanyjoes it seams some of problems already have solved. which one is still actual?

toomanyjoes commented 4 years ago

@ajahongir let's say you have a Submission model that has a relationship with a submission record but this submission record can be 1 of many different types of submissions, let's say a Student submission or a Faculty submission or a Staff submission. This would be best represented as a polymorphic relationship e.g.

class Submission < ApplicationRecord
  belongs_to :submission_record, polymorphic: true, dependent: :destroy
  ...
end
class StudentSubmission < ApplicationRecord
  has_one :submission, as: :submission_record, dependent: :destroy
  ...
class FacultySubmission < ApplicationRecord
  has_one :submission, as: :submission_record, dependent: :destroy
  ...
class StaffSubmission < ApplicationRecord
  has_one :submission, as: :submission_record, dependent: :destroy
  ...

but if you want to have a ajax rails datatable with submissions and you include a common column from the submission_record there doesn't seem to be a straightforward way to sort/search on this column.

  def submission_view_columns
    @view_columns ||= {
        id: { source: "Submission.id", searchable: false },
        submitted_on: { source: "Submission.time_submitted_at" },
        entity_number: { source: "Entity.number" },
        entity_name: { source: "Entity.name" },
        type: { source: "Submission.submission_record_type" },
        title: { source: "Submission.title" },
        total_expenses: { source: "SubmissionRecord.total_expenses" },
        num_records: { source: "SubmissionRecord.data_rows.count" },
    }
  end

The colored lines don't work, columns from polymorphic tables don't behave like other tables as far as sorting searching is concerned. It just breaks.

The below also doesn't work:

        total_expenses: { source: "Submission.submission_record.total_expenses" },
        num_records: { source: "Submission.submission_record.data_rows.count" },

Does that make sense?

Is there a less than obvious way to accomplish this? Or is it just not possible?