简体   繁体   English

Rails 5 Activerecord:如何在仪表板视图中显示每个“用户”的“记录类型”

[英]Rails 5 Activerecord: How to show 'type of' record per 'user' in a dashboard view

I'm currently struggling to refactor some old code that shows a dashboard to a manager. 我目前正在努力重构一些向经理显示仪表板的旧代码。

A User sends many Emails that relate to meal bookings - which are stored for auditing purposes mainly. 用户发送许多与餐食预订相关的电子邮件,这些电子邮件主要用于审核目的。 We track the number of emails sent per user and the value of that booking, grouped by email_type 我们会按照email_type跟踪每个用户发送的电子邮件数量和该预订的价值

At the moment ActiveRecord is getting each email, with a where clause to filter on created_at, rails is then adding to an array which is then output to a table. 当前ActiveRecord正在获取每封电子邮件,并带有一个where子句以对created_at进行过滤,然后将rails添加到一个数组中,然后将其输出到表中。 This seems really inefficient and Nginx is timing out so we can't see the results. 这似乎效率很低,Nginx正在超时,因此我们看不到结果。

I feel like using a mostly ActiveRecord with some groups will make this all so much simpler. 我觉得对某些组使用主要是ActiveRecord的东西会使事情变得更加简单。

I've just added an association to user.rb as follows - because there wasn't one in place (!) and it currently isn't being utilised: 我刚刚将一个关联添加到user.rb,如下所示-因为没有一个关联(!),并且当前没有被利用:

  has_many :emails, :foreign_key => "triggered_by_id"

Currently the MVC looks like so: 当前,MVC如下所示:

Model - email.rb: 模型-email.rb:

  scope :sent_between, -> ( start_date, end_date ) { where("emails.created_at >= ? AND emails.created_at <= ?", start_date, end_date) }

  def self.sent_today
    sent_between(DateTime.now.beginning_of_day, DateTime.now.end_of_day)
  end

  def self.metric_hash
    {
      "venue_confirmation"                    => [0, 0],
      "enquiry_confirmation"                  => [0, 0],
      "menu_verification"                     => [0, 0],
      "amendment_information"                 => [0, 0],
      "amendment_confirmation"                => [0, 0],
      "booking_confirmation_to_venue"         => [0, 0],
      "released_confirmation_to_venue"        => [0, 0],
      "cancellation_confirmation_to_venue"    => [0, 0],
      "transfer"                              => [0, 0],
      "total"                                 => [0, 0]
    }
  end

  def self.type_for_metrics(email)
    if %w(released_confirmation_to_venue cancellation_confirmation_to_venue).include?(email.email_type)
      return "transfer" if email.booking.transferred_at
  end

    email.email_type
  end

  def self.metrics(start_date, end_date)
    metrics = {}
    totals = metric_hash
    observed_bookings = Set.new

     Email.includes(:booking).select(:id, :email_type, :triggered_by_id, :booking_id).sent_between(start_date, end_date).references(:booking).select(:booking_total).find_each do |email|

      email_type = type_for_metrics(email)

      if totals.has_key?(email_type)
        metrics[email.triggered_by_id] ||= metric_hash
        metrics[email.triggered_by_id][email_type][0] += 1
        metrics[email.triggered_by_id][email_type][1] += email.booking.booking_total
        metrics[email.triggered_by_id]["total"][0] += 1
        metrics[email.triggered_by_id]["total"][1] += email.booking.booking_total
        totals[email_type][0] += 1
        totals[email_type][1] += email.booking.booking_total
        totals["total"][0] += 1

        # Only count the each booking once for the total value
        unless observed_bookings.include?(email.booking_id)
          totals["total"][1] += email.booking.booking_total
          observed_bookings << email.booking_id
        end
      end
    end

    results = metrics.inject({}) do |memo, row|
      if row[1]["total"][0] > 0
        if row[0]
          user = User.find(row[0])
          memo[user.name] = row[1]
        else
          memo["Sent Before Tracking"] = row[1]
        end
        memo
      end
    end
    results["Total"] = totals

    results
  end

index_controller.rb: index_controller.rb:

  def metrics
      @start = ( params[:start] && Time.zone.parse(params[:start]) ) || DateTime.now.start_of_period
      @end   = ( params[:end] && Time.zone.parse(params[:end]) ) || DateTime.now.end_of_period
      @email_metrics = Email.metrics(@start, @end)
  end

_metrics.html.erb _metrics.html.erb

<h2>Emails Sent</h2>

  <table>
    <thead>
      <tr>
        <th></th>
        <th title="New Enquiry">New</th>
        <th title="Menu Confirmation">Menu</th>
        <th title="Operator Confirmation">Confirm</th>
        <th title="Released Enquiry">Released</th>
        <th title="Cancelled Booking">Cancelled</th>
        <th title="Amendment Information">Amend Info</th>
        <th title="Amendment Confirmation">Amend Confirm</th>
        <th title="Transferred">Transfer</th>
        <th>Total</th>
      </tr>
    </thead>
    <tbody>
      <% @email_metrics.each do |key, metrics| %>
        <tr>
          <th rowspan="2"><%= key %></th>
          <td><%= metrics["venue_confirmation"][0] %></td>
          <td><%= metrics["menu_verification"][0] %></td>
          <td><%= metrics["booking_confirmation_to_venue"][0] %></td>
          <td><%= metrics["released_confirmation_to_venue"][0] %></td>
          <td><%= metrics["cancellation_confirmation_to_venue"][0] %></td>
          <td><%= metrics["amendment_information"][0] %></td>
          <td><%= metrics["amendment_confirmation"][0] %></td>
          <td><%= metrics["transfer"][0] %></td>
          <td><%= metrics["total"][0] %></td>
        </tr>
        <tr>
          <td><%= number_to_currency metrics["venue_confirmation"][1] %></td>
          <td><%= number_to_currency metrics["menu_verification"][1] %></td>
          <td><%= number_to_currency metrics["booking_confirmation_to_venue"][1] %></td>
          <td><%= number_to_currency metrics["released_confirmation_to_venue"][1] %></td>
          <td><%= number_to_currency metrics["cancellation_confirmation_to_venue"][1] %></td>
          <td><%= number_to_currency metrics["amendment_information"][1] %></td>
          <td><%= number_to_currency metrics["amendment_confirmation"][1] %></td>
          <td><%= number_to_currency metrics["transfer"][1] %></td>
          <td>N/A</td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>

My feeling is that I'd start with User.emails.etc... but I'm getting stuck with the multiple group_by and the massively complicated array currently in place. 我的感觉是我将从User.emails.etc ...开始,但是我陷入了当前存在的多个group_by和非常复杂的数组。

Here's a screenshot of how it should look. 这是它的外观的屏幕截图。 Dev environment only has one User currently. 开发环境目前只有一个用户。

Screenshot of how the data should look 数据外观的屏幕截图

I have came up a solution. 我想出了一个解决方案。

  scope :sent_between, -> ( start_date, end_date ) { where("emails.created_at >= ? AND emails.created_at <= ?", start_date, end_date) }

  def self.sent_today
    sent_between(DateTime.now.beginning_of_day, DateTime.now.end_of_day)
  end

  def self.metric_hash
    {
        "venue_confirmation"                    => [0, 0],
        "enquiry_confirmation"                  => [0, 0],
        "menu_verification"                     => [0, 0],
        "amendment_information"                 => [0, 0],
        "amendment_confirmation"                => [0, 0],
        "booking_confirmation_to_venue"         => [0, 0],
        "released_confirmation_to_venue"        => [0, 0],
        "cancellation_confirmation_to_venue"    => [0, 0],
        "transfer"                              => [0, 0],
        "total"                                 => [0, 0]
    }
  end

  def self.type_for_metrics(email)
    if %w(released_confirmation_to_venue cancellation_confirmation_to_venue).include?(email.email_type)
      return "transfer" if email.booking.transferred_at
    end

    email.email_type
  end

  def self.metrics(start_date, end_date)
    metrics = {}
    totals = metric_hash
    observed_bookings = {}

    Email.includes(:booking).select(:id, :email_type, :triggered_by_id, :booking_id).sent_between(start_date, end_date).
        references(:booking).select(:booking_total).find_each do |email|

      email_type = type_for_metrics(email)

      if totals[email_type]
        metrics[email.triggered_by_id] ||= metric_hash
        metrics[email.triggered_by_id][email_type][0] += 1
        metrics[email.triggered_by_id][email_type][1] += email.booking.booking_total
        metrics[email.triggered_by_id]["total"][0] += 1
        metrics[email.triggered_by_id]["total"][1] += email.booking.booking_total
        totals[email_type][0] += 1
        totals[email_type][1] += email.booking.booking_total
        totals["total"][0] += 1

        # Only count the each booking once for the total value
        unless observed_bookings[email.booking_id]
          totals["total"][1] += email.booking.booking_total
          observed_bookings[email.booking_id] = true
        end
      end
    end

    users = users = User.select(:id,:name).where(id: metrics.keys).group_by(&:id).transform_values{ |value| value.first }
    results = metrics.inject({}) do |memo, row|
      if row[1]["total"][0] > 0
        if row[0]
          user = users[row[0]]
          memo[user.name] = row[1]
        else
          memo["Sent Before Tracking"] = row[1]
        end
        memo
      end
    end
    results["Total"] = totals

    results
  end

What I have done here is I changed the type of observed_bookings to hash. 我在这里所做的是将observed_bookings的类型更改为哈希。 Because hashes has a blazing fast look up and inserting new item to hash is O(1) in your scenario. 因为哈希具有快速的查找功能,并且在您的方案中将新项目插入哈希是O(1)。 In your scenario, you make a look up and you want the data structure to be uniq. 在您的方案中,您进行了查找,并且希望数据结构是唯一的。 So hash is your friend. 因此,哈希是您的朋友。

I have changed totals.has_key?(email_type) to totals[email_type] . 我已经将totals.has_key?(email_type)更改为totals[email_type] Again here we have blazing look up which O(1). 在这里,我们再次大胆地查找哪个O(1)。

And lastly, because of the fact that user ids are actually metrics keys, so with just only query we can fetch the users from database and convert the result result to hash. 最后,由于用户ID实际上是metrics键,因此仅使用查询就可以从数据库中获取用户并将结果转换为哈希。

And yes, users are hash and user id is the key of hash. 是的,用户是哈希,而用户ID是哈希的关键。 So again we have a blazing fast lookup which is O(1). 因此,我们又有了一个快速的快速查找,它是O(1)。

I think this query should give what you expect in terms of response time. 我认为该查询应该给出您期望的响应时间。

I agree with you that just retrieving all emails, iterating over them and building the result set manually seems completely counterproductive. 我同意您的看法,即仅检索所有电子邮件,对其进行遍历并手动构建结果集似乎完全适得其反。

You will not be able to fix this with a single query, but instead of fetching 10000 records you would be able to solve it with a few (faster queries) and let the database do its work. 您将无法通过单个查询来解决此问题,但是除了获取10000条记录外,您还可以通过一些查询(更快的查询)来解决它,并让数据库完成工作。

I will give some examples that hopefully get you started. 我将提供一些示例,希望可以帮助您入门。

Fetching all types for a given period: 提取给定期间内的所有类型:

Email.sent_between(start_date, end_date).group(:email_type).count 

Fetching the amount of mails per user, in a given period 在给定时间内获取每个用户的邮件数量

Email.sent_between(start_date, end_date).group(:triggered_by_id).count 

This returns two things: the users and their count. 这将返回两件事:用户及其人数。 Use those users to get the result per user: 使用这些用户获得每个用户的结果:

Email.where(trigger_by_id: user_id).sent_between(start_date, end_date).group(:email_type).count 

The booking-total seems a little harder, but I think something like this should work: 预订总数似乎有点困难,但我认为类似这样的方法应该起作用:

Email.includes(:booking).sent_between(start_date, end_date).references(:booking).group(:email_type).sum(:booking_total) 

I understand this is not a complete solution, you still have to compose your "result-hash" but this should fetch your raw data a lot quicker. 我知道这不是一个完整的解决方案,您仍然必须编写“结果哈希”,但这应该可以更快地获取原始数据。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM