ORM: Object Relational Mapping with Ruby

zigzagoon1

zigzagoon1

Posted on August 2, 2023

ORM: Object Relational Mapping with Ruby

I'm going to discuss the concept of Object Relational Mapping, or ORM, using Ruby examples. I will briefly introduce Active Record, but this article is mostly about learning to create our own ORM system to show how they work and why we need them.

Uhh... ORM?

ORM stands for Object Relational Mapping. But what actually is an ORM?

An ORM is the link or bridge between our object-oriented language and our database.

When creating a website, or a game, or an app, etc., there's a front end, which contains all the code for handling user interaction and UI/UX, and a back end, which handles the storing of relevant information. This information can range from a high score in a game, or whether or not an item is in stock for a store's website- basically, the sky's the limit. These data points are stored in a database in the back end, along with some Ruby (or language of your choice) scripts to interact with that database in order to send data to the front end when it's requested.

Let's say we're using SQLite for our database. In order to store information, we need to create tables for that data. Because a website is (usually) not static but changes constantly depending on user interaction, we also need our database to be dynamic, which means being able to create, read, update, and delete both tables and rows within tables. These are known as CRUD operations, and they're the core operations necessary for building an interactive website.

Without using ORM, we would need to write out SQLite queries for each CRUD operation every time we need to use or save the data in our database. This can get very tedious very fast, and there's a lot of opportunity for errors. Fortunately, there's a better way.

Making Our Own ORM

One way we can speed things up and reduce errors in our CRUD operations using Ruby, or any other object-oriented lanuage, is to create a class (an object) that shares the same properties as a table in the database. If that's at all unclear, let's look at an example class:

class Cat
    attr_accessor :id, :name, :color, :age
end
Enter fullscreen mode Exit fullscreen mode

Using the Cat class, we would create a table in our database called cats. The cats table would have a column for id, name, color, and age. Now, every instance of the Cat class has all the information necessary to create a row in our cats table in the database!

In Ruby, it is convention to name our class a singular word, such as Cat, and the corresponding database table the lowercase plural of the same word, such as cats. With the relationship between our class and its properties and the table and columns, we can now create some class functions to perform each CRUD operation to our database, all from OOP Ruby!

For example, let's look at our Triangle class with some ORM capabilities.

class Cat
    attr_accessor: :id, :name, :color, :age

    #Upon a new cat instance being created, initialize its 
    #attributes. Note that we assume id is an autoincrementing
    #primary key, so we set it to nil initially
    def initialize(id: nil, name:, color:, age:)
        @id = id
        @name = name
        @color = color
        @age = age
    end

    #class method to create the cats table 
    def self.create_table
        sql = <<-SQL
          CREATE TABLE IF NOT EXISTS cats (
            id INTEGER PRIMARY KEY,
            name TEXT,
            color TEXT,
            age INTEGER
            )
            SQL
        DB[:conn].execute(sql)
    end

    #class method to create a new instance of a cat, and save that  
    #cat to the database
    def self.create(name:, color:, age:)
        cat = Cat.new(name: name, color: color, age: age)
        cat.save
    end

    #instance method to call on an instance of cat that saves the    
    #cat as a row in the cats table
    def save
        sql = <<-SQL
          INSERT INTO cats (name, color, age)
          VALUES (?, ?)
          SQL
        DB[:conn].execute(sql, self.name, self.color, self.age)
        self.id = DB[:conn].execute("SELECT last_insert_rowid() 
          FROM cats")[0][0]
        #return the newly created instance
        self
    end

    #create a new cat instance from a row in the database
    def self.new_from_db(row)
        self.new(id: row[0], name: row[1], color: row[2] age: 
          row[3])
    end

    #search the database for a single cat with the matching name,     
    #and create an instance that corresponds to the row
    def self.find_by_name(name)
        sql = <<-SQL
          SELECT *
          FROM cats
          WHERE name = ?
          LIMIT 1
          SQL

        DB[:conn].execute(sql, name).map do |row|
            self.new_from_db(row)
        end.first
    end
end
Enter fullscreen mode Exit fullscreen mode

Okay, so that was a lot at once! The comments explain what each function does, but there are a few things there that may not be familiar.

First of all, you may have noticed that in those methods we call DB[:conn].execute(sql). In order to query the database, we have to establish a connection to the database. Most commonly, that connection is defined in a file called environment.rb that is in a folder called config. In environment.rb, we can create the connection with the database with the following line of code:

DB = {conn: SQLite3::Database.new("db/cats.db") }

In order to do this, SQLite3 must be installed in your project.

By the way, in case you were wondering what sql = <<-SQL... does, well <<- is called a heredoc. It's a way for us to have a multi-line string that keeps the format we supply it. Whatever immediately follows the <<- is what signals that it's the end of the string. So in our case, the indicator is SQL, and we must put SQL at the end of the string query we write when using a heredoc.

Now, our Cat class can easily create Cat instances that can be saved to the cats table in the database as a new row. All we have to do is:

cat = Cat.new(name: "Ziggy", color: "orange", age: 7)
cat.save
Enter fullscreen mode Exit fullscreen mode

As you can imagine, this is much simpler and easier to work with than it would be without the mapped relationship between a Ruby class and SQLite table. For one, not having to write out the SQL query multiple times can save a lot of time and reduce the chance for errors!

Active Record

If you were already thinking that these ORM things were pretty useful for saving time and reducing complexity, I've got some great news for you. If you use Ruby, there's a very handy little Gem called Active Record that makes everything we did in the previous section even easier! While there are still cases where a custom ORM or a different ORM gem would be useful, most of the time Active Record can be a great asset for your project.

I'm not going to too much in to Active Record, but it provides ORM methods for CRUD operations like the ones we created in the example so we don't even have to write out those methods in each class we want mapped to our database. We simply follow a few Active Record conventions, and it'll do all the work for us!

If you're working with Ruby and databases for a back end and aren't using Active Record, I highly recommend switching to it. It's easy to add to your project and isn't difficult to learn how to use it. I recommend checking out the official guides at this link to learn the ins and outs of how to use it, and you can thank me later!

That's all for this article; however, there's a lot more to learn about ORMs! For example, often we would have error handling built in to our methods, as well as data validation to ensure that the correct type of data is being saved, or to prevent null entries. Active Record includes some data validation methods, which you can read about here.
You can learn more about error handling here.

Please feel free to comment if you noticed any mistakes or have any questions! Until next time :)

💖 💪 🙅 🚩
zigzagoon1
zigzagoon1

Posted on August 2, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related