-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
115: Practice Exercise: Building a Simple ActiveRecord-Style ORM
A few years ago, I worked with a developer who was tasked with migrating a legacy system that used raw SQL for everything. I remember watching him spend an entire afternoon writing nearly identical INSERT and UPDATE statements for five different tables. He was exhausted, not because the logic was hard, but because the repetition was soul-crushing. I told him, "You're manually mapping data from a hash to a string. Why not just make the object responsible for its own persistence?" That "aha!" moment is exactly why the ActiveRecord pattern exists. It stops you from thinking in rows and columns and lets you think in objects.
Mapping Ruby Classes to Database Tables
The magic of an ORM (Object-Relational Mapper) is the convention it establishes. Instead of telling Ruby which table to use for every single query, we can use the class name itself. If you have a class called User, it's a safe bet that your table is called users. By leveraging self.class.name and a bit of string manipulation, we can automate the table targeting. I've always found that the most elegant way to handle this is to create a base class that handles the heavy lifting, which your specific models then inherit from.
In a real ActiveRecord implementation, the library queries the database schema to know which columns exist. For our simplified version, we'll store attributes in a hash. This keeps the object flexible; we don't need to hardcode every single column as an instance variable, which would be a nightmare to maintain as your schema grows.
The Power of method_missing for Dynamic Attributes
If you've used ActiveRecord, you've noticed you can call user.email even if you never defined an attr_accessor :email in your class. This is where Ruby's metaprogramming shines. By overriding method_missing, we can intercept calls to methods that don't exist and check if they match a key in our attributes hash. It feels like magic the first time you see it, but it's really just a safety net.
def method_missing(method_name, *args, &block)
if attributes.has_key?(method_name)
attributes[method_name]
elsif attributes.has_key?("#{method_name}=")
# Handle setters here
else
super
end
end
I usually warn people not to over-use method_missing because it can make debugging a headache, but for an ORM, it's the gold standard. It allows your Ruby objects to evolve alongside your database without requiring a code change every time you add a column to a table.
Translating Object State to SQL
The final piece of the puzzle is the save method. The logic here is binary: either the object is new and needs an INSERT, or it already has an ID and needs an UPDATE. This is where you translate the internal attributes hash into a SQL string. You'll want to iterate over the hash keys to build your column list and values list dynamically.
When I build these prototypes, I always emphasize the importance of the where clause. A simple find(id) method is just a wrapper around SELECT * FROM table WHERE id = x. The goal is to ensure that the developer interacting with the class never has to write a single line of SQL; they should just be manipulating Ruby objects and calling .save when they're done.
📋 Practical Task
Exercise: Implementing the MiniORM Base Class
Your task is to build a simplified version of an ActiveRecord-style ORM. You need to create a base class called MiniORM and a subclass called User that uses it to interact with a conceptual database.
Requirements:
- Create a
MiniORMclass that initializes with anattributeshash. - Implement
method_missinginMiniORMso that you can access attribute values via method calls (e.g.,user.nameinstead ofuser.attributes[:name]). - Implement a class method
MiniORM.find(id)that returns a new instance of the class with a mocked set of data (simulate a database hit). - Implement an instance method
save. This method should print a simulated SQL string to the console:- If the object has an
:id, print:"UPDATE [table_name] SET [columns] WHERE id = [id]" - If the object does NOT have an
:id, print:"INSERT INTO [table_name] ([columns]) VALUES ([values])"
- If the object has an
- The
table_nameshould be automatically derived from the class name (e.g.,Userbecomes"users").
Testing your implementation:
user = User.new(name: "Alice", email: "alice@example.com")
puts user.name # Should output: Alice
user.save # Should output: INSERT INTO users (name, email) VALUES (Alice, alice@example.com)
existing_user = User.find(1)
existing_user.name = "Bob" # Implement a basic setter or handle via attributes hash
existing_user.save # Should output: UPDATE users SET name = Bob... WHERE id = 1There are no comments for now.