简体   繁体   中英

Ruby on Rails's ActiveRecord vs DBMS scripts

I'm learning Ruby on Rails and following some guides to get to know the framework.

Currently I'm reading about ActiveRecord migrations and it seems to be very good to keep a track of changes during development phases, specially when you work with agile and requirements change often.

However, I believe that if you have fine-tune your DB, is preferable to work with vendor-specific scripts (MySQL, Postgres, etc.).

Needless to say, it's not my intention to make this post opinion-based, so my first question is if you know pros and cons about this approaches to build an application with Rails ( ActiveRecord migrations vs DBMS specific scripts). I've been searching on internet but I didn't found any comparison.

Also, I'd like to know if there is any risk, something I should pay attention to, or avoid to do if I need to combine these 2 approaches.

Thanks in advance for your comments/answers. Best regards

TLDR:

You don't want to use 3rd party tools to manually manage your database. Try keeping your database code as close to Rails' migrations as possible.

The story

Rails migrations are just great if you are doing a CRUD application where you don't need any complex logic on either app or database side. This feature allows you to write incremental changes to your database and roll them back without disrupting your application in production. Say you have an "users" table and want to add a field, let's call it "second_email_address". You would do this:

class AddSecondEmailAddressToUsers < ActiveRecord::Migration[5.2]

  def self.up
    add_column :users, :second_email_address, :string
  end

  def self.down
    remove_column :users, :second_email_address, :string
  end

end

Rails keeps track of your database schema "version" based on migration file names and can tell you exactly where you are and enables you to roll back anything you change your mind about. You can add or remove this column without losing any of your data in your table except if course the data stored in this column. This is very nice for basic use cases.

Things tend to get a bit more complicated when you want to get a bit more personal with your database.

Take triggers for example, and let's say you have a table "Shops" that has_many :users and you want to keep track of the user count in an integer column on the shops table. The Rails convention over configuration paradigm dictates that you do something like this in your users model:

after_create :increase_shop_user_count
def increase_shop_user_count
  self.shop.user_count+=1
end

With a trigger, you'd do...

create or replace trigger increase_shop_user_count
  after insert on users
  for each row
  begin
    update shops set user_count = user_count + 1 where shops.id = NEW.shop_id
  end;

The performance gain by using the trigger is astonishing. In most real life situations however you don't care about this and you're happy to trade off a few ms of latency for the convenience of having it all in the Rails application, the Rails way. But I guarantee you will change your mind when you have a few thousand shops with a few hundred thousand users each, and lots of other similar or even more complicated features within your application. That's right, having a successful application means you'll have to get your hands dirty (and learn to ignore mean comments from people who will inform you you will boil in lava until the end of eternity for disrespecting the "Rails way", nts nts).

In my application I have about 20K lines of SQL in triggers, procedures, scheduled events that were pulled out of the Rails application code base and moved to the database layer. Sure, it adds a certain degree of complexity from several respects (writing the SQL, migrating, testing...) but at the end of the month the company pays $20K less in EC2 bills every month.

If you wanted to do something like this directly with phpmyadmin for instance you'd have to do every single operation manually, however as your application grows in complexity you'll get overwhelmed and this translates to support calls at midnight and downtime. Fortunately you can still use Rails migrations with triggers/stored procedures and so on.

You can write your SQL in separate migration files following the example above. You can use a gem such as hair_trigger to define your triggers inside models and have them exported to schema.rb or structure.sql (although this doesn't cover and there seems to be no equivalent for procedures/functions and events). You can also switch your schema to SQL and you get a big fat SQL file with all your code, that's a bit too much to manage.

But since this is no longer about defining tables and columns, but more like application features, you can also have a sql/ directory inside your app/ directory and group your SQL code in there, for instance:

# app/sql/mysql/triggers/increase_shop_user_count.sql
    create or replace trigger increase_shop_user_count
      after insert on users
      for each row
      begin
        update shops set user_count = user_count + 1 where shops.id = NEW.shop_id
      end;

Then in a Rails migration file you do:

class IncreaseShopUserCountTrigger < ActiveRecord::Migration[5.2]
  def self.up
    execute File.read( Rails.root.join("app","sql","mysql","triggers","increase_shop_user_count.sql"))
  end
  def self.down
    execute "drop trigger increase_shop_user_count"
  end
end

Of course, when you have hundreds of these babies you want to have a discovery mechanism in place so you don't execute File.read... every trigger/procedure like a barbarian.

Bottom line: not bad at all! So Rails migrations enable you to do incremental changes to your databases and keep them all organized, versioned and easy to manage!

But how about testing?

Well, once again you'd go a bit out of the nice paved road that is the Rails convention. You'd test a model's behavior like this:

it "changes shop user count after creation" do
  s = create :shop
  u = create :user, shop: s
  expect(s.reload.user_count).to eq 1 # because our database-side magic changed the count!
end

Later edit: the user counter example above can of course be refactored using AR's counter caching feature, but hopefully it proves a point.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM