我正在编写一个用户输入日期的应用程序,然后系统从外部API获取该周的历史天气数据(我假设星期三是整周的代表)。出于某些原因,我不想为每个日期进行实时调用 - 我想获取它一次并在现场持续存在。
在Spring中,我将大部分内容放入服务层。由于我是Rails的新手,我不知道在哪里放置某些逻辑,但这是我的建议:
WeatherController
def create
transform date entered by user to Wednesday of the same week.
Check if there is a already record for that date, if not, fetch the JSON from external API.
Parse JSON to Ruby object, save.
Return the weather data.
WeatherModel
validate if the date is indeed Wednesday
validate if entered date is unique
答案 0 :(得分:1)
在rails中,我更喜欢创建PORO(计划旧的ruby对象)来处理我的应用程序中的大多数核心逻辑。通过这样做,我们可以使控制器变得简单,并且我们的模型没有与将数据保存到数据库无关的逻辑。如果你不从我们的模型中保留不必要的逻辑,它们将变得臃肿并且极难测试。
我最常用的两个PORO模式是actions
和services
。
actions
通常直接与一个控制器动作相关联。
以你的例子来创建一个。我们将创建一个WeatherCreator
类。我喜欢疯狂明确的名字。您问WeatherCreator
什么?它当然会创建一个Weather
记录!
# app/actions/weather_creator.rb
class WeatherCreator
attr_reader :weather
def initialize(args={})
@date = args.fetch(:date)
@weather = Weather.new
end
def create
build_record
@weather.save
end
private
def build_record
# All of your core logic goes here!
# Plus you can delegate it out to various private methods in the class
#
# transform date entered by user to Wednesday of the same week.
# Check if there is a already record for that date, if not, fetch the JSON from external API.
# Parse JSON to Ruby object, save.
#
# Add necessary data to your model in @weather
end
end
然后在我们的控制器中,我们可以使用action
类
# app/controllers/weather_controller.rb
class WeatherController < ApplicatonController
def create
creator = WeatherCreator.new(date: params[:date])
if creator.create
@weather = creator.weather
render :new
else
flash[:success] = "Weather record created!"
redirect_to some_path
end
end
end
现在你的控制器很简单。
这样做的好处是您的测试工作可以只关注action
逻辑对象及其界面。
# spec/actions/weather_creator_spec.rb
require 'rails_helper'
RSpec.describe WeatherCreator do
it "does cool things" do
creator = WeatherCreator.new(date: Time.zone.now)
creator.create
expect(creator.weather).to # have cool things
end
end
另一方面, service
个对象将存在于app/services/
中。不同之处在于这些对象在应用程序的许多地方使用,但逻辑和测试实践的相同隔离适用。
根据您的应用程序,您可以为各种目的创建不同类型的POROS,因为一般service
对象类别也会失控。
为了清楚起见,您可以使用不同的命名做法。因此,我们可以使用WeatherCreator
课程,而不是将其称为WeatherCreatorAction
或Action::WeatherCreator
。有些可以使用服务SomeLogicService
或Service::SomeLogic
。
尽可能使用您喜好和风格的套房。干杯!
答案 1 :(得分:1)
通常,我不会将逻辑放在create
动作中。即使您正在制作某些内容,您网站的用户也会要求您show
天气。用户应该忘记您从哪里获取信息以及如何缓存信息。
一种选择是在show动作中使用Rails caching。在该操作中,您将对API进行阻塞调用,然后Rails将返回值存储在缓存存储中(例如Redis)。
def show
date = Date.parse params[:date]
@info_to_show = Rails.cache.fetch(cache_key_for date) do
WeatherAPIFetcher.fetch(date)
end
end
private
def cache_key_for(date)
"weather-cache-#{date + (3 - date.wday)}"
end
上面的选项1将使您已经积累的数据访问有些尴尬(例如,对于统计数据,图表等)。此外,它在您等待API端点的响应时阻止服务器。如果这些都不是问题,您应该考虑选项1,因为它非常简单。如果您需要更多,以下是存储您在数据库中提取的数据的建议。
我建议存储数据的模型和检索数据的异步作业。请注意,您需要为WeatherFetcherJob设置ActiveJob。
# migration file
create_table :weather_logs do |t|
t.datetime :date
# You may want to use an enumerized string field `status` instead of a boolean so that you can record 'not_fetched', 'success', 'error'.
t.boolean :fetch_completed, default: false
t.text :error_message
t.text :error_backtrace
# Whatever info you're saving
t.timestamps
end
add_index :weather_logs, :date
# app/models/weather_log.rb
class WeatherLog
# Return a log record immediately (non-blocking).
def self.find_for_week(date_str)
date = Date.parse(date_str)
wednesday_representative = date + (3 - date.wday)
record = find_or_create_by(date: wednesday_representative)
WeatherFetcherJob.perform_later(record) unless record.fetch_completed
record
end
end
# app/jobs/weather_fetcher_job.rb
class WeatherFetcherJob < ActiveJob::Base
def perform(weather_log_record)
# Fetch from API
# Update the weather_log_record with the information
# Update the weather_log_record's fetch_completed to true
# If there is an error - store it in the error fields.
end
end
然后,在控制器中,您可以依赖API是否已完成以决定向用户显示的内容。这些是广泛的,你必须适应你的用例。
# app/controllers/weather_controller
def show
@weather_log = WeatherLog.find_for_week(params[:date])
@show_spinner = true unless @weather_log.fetch_completed
end
def poll
@weather_log = WeatherLog.find(params[:id])
render json: @weather_log.fetch_completed
end
# app/javascripts/poll.js.coffee
$(document).ready ->
poll = ->
$.get($('#spinner-element').data('poll-url'), (fetch_in_progress) ->
if fetch_in_progress
setTimeout(poll, 2000)
else
window.location = $('#spinner-element').data('redirect-to')
)
$('#spinner-element').each -> poll()
# app/views/weather_controller.rb
...
<% if @show_spinner %>
<%= content_tag :div, 'Loading...', id: 'spinner-element', data: { poll_url: poll_weather_path(@weather_log), redirect_to: weather_path(@weather_log) } %>
<% end %>
...
答案 2 :(得分:0)
我会给你一些有趣的方式来实现简单有趣的方式。你可以像书签逻辑一样:
例如:
书签的工作原理如何?用户向书签添加URL,服务器保存该书签的数据,当另一个用户尝试将相同的URL添加到书签时,服务器不会将URL保存到书签,因为它是重复的书签。它只是,服务器发现书签并分配给该用户。并再次为所有尝试添加相同网址的用户添加书签。
<强>天气:强>
在您的情况下,您所需要的只是:如果用户请求该城市的天气并且您没有该数据,则从api获取将其提供给用户并将其保存到DB。如果另一个人会请求同一个城市,现在只需从DB回复而不是第三方API。您需要的只是在请求数据时更新数据。