ActiveRecord::Base.transaction: all your writes, or none of them
A transaction the earlier courses relied on, without writing one
ActiveRecord migrations: multi-table showed that Rails wraps an ordinary migration's whole change method in one database transaction, so several create_table calls either all land or none do. Nobody wrote that transaction by hand; Rails supplied it. This course covers the same guarantee written explicitly in application code. You need it when more than one related write has to succeed or fail together and nothing supplies the transaction for you.
This app's code, doing that
Enrollment::CompleteLessonService is the service that runs when a student finishes a lesson:
# app/services/enrollment/complete_lesson_service.rb (excerpt)
completion = ::Enrollment::LessonCompletion.new(user: user, lesson: lesson)
ActiveRecord::Base.transaction do
return failure(next_lesson, completion.errors.full_messages) unless completion.save
xp_before = ::Enrollment::ExperiencePoint.total_for(user.id)
lesson_xp = ::Enrollment::AwardXpService.call(
user: user, source: "lesson_completion", course: lesson.course, idempotency_key: "lesson_completion:#{lesson.id}"
)
::Enrollment::UpdateStreakService.call(user: user)
course_result = ::Enrollment::CompleteCourseService.call(user: user, course: lesson.course)
# ... then EvaluateUnlocksService awards any new badges
end
One call produces a LessonCompletion row, an XP award, a streak update, a possible course completion and possible badges: four or five separate writes to separate tables. Without a transaction around them, a failure partway through could leave a student with XP but no completion recorded, or a completion with no XP. A dropped database connection would do it, and so would an unrelated bug in a later service. Nothing in the app would know how to repair that state.
What the guarantee covers
ActiveRecord::Base.transaction do ... end is one call, and what it buys is specific: every write inside the block becomes durable together, at the same moment, or none of them do. The database transactions the migrations courses leaned on for schema changes give ordinary rows the same all-or-nothing property. Either all of Enrollment::CompleteLessonService's writes land the instant the block finishes cleanly, or none of them do. If something inside goes wrong, the database looks as if the block never ran.
What "goes wrong" means here
That last sentence hides the rest of this course. Not every way a block can end early triggers the rollback. The next lesson tests which ones do, and the code above has a line, return failure(...) unless completion.save, sitting on the boundary between the two.
Key takeaways
- ActiveRecord migrations: multi-table relied on this guarantee, supplied automatically, for schema changes; this course is the same mechanism written by hand in application code.
- This app's
Enrollment::CompleteLessonServicewraps several separate writes (a lesson completion, XP, a streak update, a course completion, badges) in oneActiveRecord::Base.transaction do ... endblock. - The guarantee is all or nothing: every write inside the block becomes durable together, or, if anything inside fails, the database looks as if the block never ran.
- Not every way a block can end counts as failing. That distinction, and the line in this app's code that sits on it, is the next lesson.
Keep going with lesson 2
What triggers a rollback
Tests three ways to leave a transaction block: a raised exception rolls back, while a bare return commits.
A free account also gives you the quizzes, your progress and notes, and the discussion.