Ho un lavoro Ruby on Rails Model che sospetto sia inefficiente , difficile da mantenere e pieno di SQL non necessario unire le query . Voglio ottimizzare e refactor questo modello (Quiz.rb) per rispettare le best practice di Rails, ma non sono sicuro di come dovrei farlo.
L'app Rails è un gioco che ha Missions
con molti Stages
. User
s completa Stage
s rispondendo Questions
che ha Answers
corretto o errato. Quando User
tenta di completare uno stage rispondendo alle domande, User
ottiene una voce Quiz
con molti Attempt
s. Ogni Attempt
registra un Answer
inviato per quel Question
all'interno di Stage.
Un utente completa uno stage o una missione ottenendo ogni Attempt
corretto e il loro progresso viene tracciato aggiungendo una nuova voce a UserMission
& UserStage
join tabelle.
Tutte queste funzioni funzionano, ma sfortunatamente il Quiz.rb
Model è stato trasformato per gestire quasi tutto esclusivamente. I callback sono iniziati con 'Quiz.rb', e poiché non ero sicuro di come lasciare il modello Quiz
durante un aggiornamento multi-modello, ho fatto ricorso a Console Rails per avere il @quiz
variabile di istanza tramite self.some_method
fa tutto il pesante sollevamento a recupera ogni valore di dati per la logica di business del gioco; risultante in ampie query di join estese che "ballano" attorno allo schema del database.
Il modello Quiz.rb
che annusa:
class Quiz < ActiveRecord::Base
belongs_to :user
has_many :attempts, dependent: :destroy
before_save :check_answer
before_save :update_user_mission_and_stage
accepts_nested_attributes_for :attempts, :reject_if => lambda { |a| a[:answer_id].blank? }, :allow_destroy => true
#Checks every answer within each quiz, adding +1 for each correct answer
#within a stage quiz, and -1 for each incorrect answer
def check_answer
stage_score = 0
self.attempts.each do |attempt|
if attempt.answer.correct? == true
stage_score += 1
elsif attempt.answer.correct == false
stage_score - 1
end
end
stage_score
end
def winner
return true
end
def update_user_mission_and_stage
#######
#Step 1: Checks if UserMission exists, finds or creates one.
#if no UserMission for the current mission exists, creates a new UserMission
if self.user_has_mission? == false
@user_mission = UserMission.new(user_id: self.user.id,
mission_id: self.current_stage.mission_id,
available: true)
@user_mission.save
else
@user_mission = self.find_user_mission
end
#######
#Step 2: Checks if current UserStage exists, stops if true to prevent duplicate entry
if self.user_has_stage?
@user_mission.save
return true
else
#######
##Step 3: if step 2 returns false:
##Initiates UserStage creation instructions
#checks for winner (winner actions need to be defined) if they complete last stage of last mission for a given orientation
if self.passed? && self.is_last_stage? && self.is_last_mission?
create_user_stage_and_update_user_mission
self.winner
#NOTE: The rest are the same, but specify conditions that are available to add badges or other actions upon those conditions occurring:
##if user completes first stage of a mission
elsif self.passed? && self.is_first_stage? && self.is_first_mission?
create_user_stage_and_update_user_mission
#creates user badge for finishing first stage of first mission
self.user.add_badge(5)
self.user.activity_logs.create(description: "granted first-stage badge", type_event: "badge", value: "first-stage")
#If user completes last stage of a given mission, creates a new UserMission
elsif self.passed? && self.is_last_stage? && self.is_first_mission?
create_user_stage_and_update_user_mission
#creates user badge for finishing first mission
self.user.add_badge(6)
self.user.activity_logs.create(description: "granted first-mission badge", type_event: "badge", value: "first-mission")
elsif self.passed?
create_user_stage_and_update_user_mission
else self.passed? == false
return true
end
end
end
#Creates a new UserStage record in the database for a successful Quiz question passing
def create_user_stage_and_update_user_mission
@nu_stage = @user_mission.user_stages.new(user_id: self.user.id,
stage_id: self.current_stage.id)
@nu_stage.save
@user_mission.save
self.user.add_points(50)
end
#Boolean that defines passing a stage as answering every question in that stage correct
def passed?
self.check_answer >= self.number_of_questions
end
#Returns the number of questions asked for that stage's quiz
def number_of_questions
self.attempts.first.answer.question.stage.questions.count
end
#Returns the current_stage for the Quiz, routing through 1st attempt in that Quiz
def current_stage
self.attempts.first.answer.question.stage
end
#Gives back the position of the stage relative to its mission.
def stage_position
self.attempts.first.answer.question.stage.position
end
#will find the user_mission for the current user and stage if it exists
def find_user_mission
self.user.user_missions.find_by_mission_id(self.current_stage.mission_id)
end
#Returns true if quiz was for the last stage within that mission
#helpful for triggering actions related to a user completing a mission
def is_last_stage?
self.stage_position == self.current_stage.mission.stages.last.position
end
#Returns true if quiz was for the first stage within that mission
#helpful for triggering actions related to a user completing a mission
def is_first_stage?
self.stage_position == self.current_stage.mission.stages_ordered.first.position
end
#Returns true if current user has a UserMission for the current stage
def user_has_mission?
self.user.missions.ids.include?(self.current_stage.mission.id)
end
#Returns true if current user has a UserStage for the current stage
def user_has_stage?
self.user.stages.include?(self.current_stage)
end
#Returns true if current user is on the last mission based on position within a given orientation
def is_first_mission?
self.user.missions.first.orientation.missions.by_position.first.position == self.current_stage.mission.position
end
#Returns true if current user is on the first stage & mission of a given orientation
def is_last_mission?
self.user.missions.first.orientation.missions.by_position.last.position == self.current_stage.mission.position
end
end
La mia domanda
Attualmente il mio server Rails impiega circa 500ms a 1 secondo per elaborare un'azione @quiz.save
singola. Sono fiducioso che la lentezza qui sia dovuta al codice sciatto, non alla cattiva progettazione del database ERD.
Che aspetto ha ? E in particolare:
-
Devo usare le unire query per recuperare valori come ho fatto qui, o è meglio istanziare nuovi oggetti all'interno del modello? O mi manca una soluzione migliore?
-
In che modo
update_user_mission_and_stage
può essere refactored per seguire le best practice?
Codice rilevante per riferimento:
quizzes_controller.rb
w / Controller Route Avvio callback:
class QuizzesController < ApplicationController
before_action :find_stage_and_mission
before_action :find_orientation
before_action :find_question
def show
end
def create
@user = current_user
@quiz = current_user.quizzes.new(quiz_params)
if @quiz.save
if @quiz.passed?
if @mission.next_mission.nil? && @stage.next_stage.nil?
redirect_to root_path, notice: "Congratulations, you have finished the last mission!"
elsif @stage.next_stage.nil?
redirect_to [@mission.next_mission, @mission.first_stage], notice: "Correct! Time for Mission #{@mission.next_mission.position}", info: "Starting next mission"
else
redirect_to [@mission, @stage.next_stage], notice: "Answer Correct! You passed the stage!"
end
else
redirect_to [@mission, @stage], alert: "You didn't get every question right, please try again."
end
else
redirect_to [@mission, @stage], alert: "Sorry. We were unable to save your answer. Please contact the admministrator."
end
@questions = @stage.questions.all
end
private
def find_stage_and_mission
@stage = Stage.find(params[:stage_id])
@mission = @stage.mission
end
def find_question
@question = @stage.questions.find_by_id params[:id]
end
def quiz_params
params.require(:quiz).permit(:user_id, :attempt_id, {attempts_attributes: [:id, :quiz_id, :answer_id]})
end
def find_orientation
@orientation = @mission.orientation
@missions = @orientation.missions.by_position
end
end
Panoramica delle relazioni del database ERD pertinente:
Missione - > Stage - > Domanda - > Risposta - > Tentativo < - Quiz < - Utente
Missione - > UserMission < - User
Stage - > UserStage < - User
Altri modelli:
Mission
.RB
class Mission < ActiveRecord::Base
belongs_to :orientation
has_many :stages
has_many :user_missions, dependent: :destroy
has_many :users, through: :user_missions
#SCOPES
scope :by_position, -> {order(position: :asc)}
def stages_ordered
stages.order(:position)
end
def next_mission
self.orientation.missions.find_by_position(self.position.next)
end
def first_stage
next_mission.stages_ordered.first
end
end
Stage
.RB:
class Stage < ActiveRecord::Base
belongs_to :mission
has_many :questions, dependent: :destroy
has_many :user_stages, dependent: :destroy
has_many :users, through: :user_stages
accepts_nested_attributes_for :questions, reject_if: :all_blank, allow_destroy: true
def next_stage
self.mission.stages.find_by_position(self.position.next)
end
end
Question
.RB
class Question < ActiveRecord::Base
belongs_to :stage
has_many :answers, dependent: :destroy
accepts_nested_attributes_for :answers, :reject_if => lambda { |a| a[:body].blank? }, :allow_destroy => true
end
Answer
.RB:
class Answer < ActiveRecord::Base
belongs_to :question
has_many :attempts, dependent: :destroy
end
Attempt
.RB :
class Attempt < ActiveRecord::Base
belongs_to :answer
belongs_to :quiz
end
User
.RB:
class User < ActiveRecord::Base
belongs_to :school
has_many :activity_logs
has_many :user_missions, dependent: :destroy
has_many :missions, through: :user_missions
has_many :user_stages, dependent: :destroy
has_many :stages, through: :user_stages
has_many :orientations, through: :school
has_many :quizzes, dependent: :destroy
has_many :attempts, through: :quizzes
def latest_stage_position
self.user_missions.last.user_stages.last.stage.position
end
end
UserMission
.RB
class UserMission < ActiveRecord::Base
belongs_to :user
belongs_to :mission
has_many :user_stages, dependent: :destroy
end
UserStage
.RB
class UserStage < ActiveRecord::Base
belongs_to :user
belongs_to :stage
belongs_to :user_mission
end