Ruby on Rails One Database Multiple Schema 적용


Rails 6 부터 Multiple Database가 지원된다. 그 이전에는 octopus를 이용해서 했지만, 이제는 더 이상 그럴 필요가 없게 되었다.

Multiple Databases with Active Record

위 가이드 대로하면 Replica 적용도 쉽게 가능하며, Multiple Schema 적용도 가능하다. Schema 파일과 Migration 파일들을 database 별로 따로 관리가 가능해진다.

이렇게 할 경우 다른 Schema에 있는 같은 이름의 Model을 처리할려면 Model명을 수정해야 한다. 그리고 서로 다른 Schema의 Model들이 하나의 폴더 app/models에서 관리된다.

Active Record Basics - Overriding the Naming Conventions

애시당초 Multiple Database를 사용하지 않고 Namespace로 나누는 방법도 있다.

Advanced Rails model generators

rails g model admin/user

이렇게 할 경우 admin_user 라는 table이 생성되고, Admin::User 라는 Model이 만들어진다. 폴더도 app/models/admin안에 user.rb가 만들어진다.

위 두가지 방법을 섞어서 사용하고 싶었다. 물리적으로 하나의 Database에 여러 Schema(mysql에서는 database라는 resource로 사용중)를 사용할 경우 조금 더 편리하게 사용하고 싶었다. 그러면서도 서로 다른 Schema의 Model들을 다른 폴더로 관리하고 싶었다. 그래서 그 방법을 시도해 보았고, 성공해서 내용을 공유하고자 한다. 방법이 완전 우아하지는 않지만, 딱 한 번의 customizing 작업만이 추가될 뿐 나머지는 나름 우아하게 동작한다.

설명을 시작하기 전에 먼저 Schema를 main, second로 나눌 것이며 main에는 User 모델이 있는 상태라는 것을 가정하겠다.

1. Multiple Database 설정

물리적으로 같은 Database라 하더라도 서로 다른 Schema를 관리하려면 Database를 나눠야 한다.

  • database.yml
development:
  main: &default
    adapter: mysql2
    encoding: utf8mb4
    username: <%= ENV.fetch("MYSQL_USERNAME") { "root" } %>
    password: <%= ENV.fetch("MYSQL_PASSWORD") { "admin" } %>
    host: <%= ENV.fetch("MYSQL_HOST") { "127.0.0.1" } %>
    port: <%= ENV.fetch("MYSQL_PORT") { 3306 } %>
    timeout: 5000
    reconnect: <%= ENV.fetch("MYSQL_RECONNECT", true) == "true" %>
    pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
    database: <%= ENV.fetch("DEBUG_PRODUCTION", "0") == "1" ? "main" : "main_development" %>
  second: &default_second
    <<: *default
    database: <%= ENV.fetch("DEBUG_PRODUCTION", "0") == "1" ? "second" : "second_development" %>
    migrations_paths: db/second_migrate

test:
  main:
    <<: *default
    database: "main_test"
  second:
    <<: *default_second
    database: "second_test"

production:
  main:
    <<: *default
    database: "main"
  second:
    <<: *default_second
    database: "second"

이제 rails db:drop db:create 등을 실행하면 2개의 main, second 둘 다 관리된다. 기존에는 db/schema.rb에 schema 상태가 기록되었다면 이제부터는 db/main_schema.rb, db/second_schema.rb에서 각각 관리된다. migration file들이 저장되는 폴더는 mian은 별도로 설정하지 않아서 db/migrate에 저장되며, second는 위에서 설정한대로 db/second_migrate에 저장된다. main도 비슷한 이름을 따르게 할려면 migrations_paths를 설정하면 될 것이다.

rails db:drop db:create

Dropped database ‘main_development’ Dropped database ‘second_development’ Dropped database ‘main_test’ Dropped database ‘second_test’ Created database ‘main_development’ Created database ‘second_development’ Created database ‘main_test’ Created database ‘second_test’

2. Second Schema에 Model 생성

rails generate model item name:string price:integer user:references --database second

이렇게 할 경우 second에는 user라는 Model이 없으므로 오류가 발생한다.

== 20201221043104 CreateItems: migrating ====================================== – create_table(:items) rails aborted! StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Table ‘second_development.users’ doesn’t exist /Users/jayden/git/sj/backend_boilerplates/rails/db/second_migrate/20201221043104_create_items.rb:3:in change' /Users/jayden/git/sj/backend_boilerplates/rails/bin/rails:9:in <top (required)>’ /Users/jayden/git/sj/backend_boilerplates/rails/bin/spring:15:in <top (required)>' bin/rails:3:in load’ bin/rails:3:in `<main>’

그래서 Migration File을 아래와 같이 수정한 후에 rails db:migrate를 실행하자.

class CreateItems < ActiveRecord::Migration[6.0]
  def change
    create_table :items do |t|
      t.string :name
      t.integer :price
      t.references :user, null: true, foreign_key: false, index: false

      t.timestamps
    end
  end
end
rails db:migrate

이렇게하면 Item.rb가 생기기는 하지만 ApplicationRecord를 상속받고 있다. 그래서 rails c로 들어가서 Item.create(name: 'aaa', price: 1)를 실행해도 items 테이블이 없다고 오류가 발생한다.

ActiveRecord::StatementInvalid: Mysql2::Error: Table ‘main_development.items’ doesn’t exist

item.rb 파일을 다음과 같이 수정한다.

class Item < ApplicationRecord
  connects_to database: { writing: :second }
  belongs_to :user, optional: true
end

이제는 Item.create(name: 'aaa', price: 1) 이렇게 실행하면 정상적으로 생성된다. 하지만 이것보다는 ApplicationRecord를 상속받은 다음 SecondRecord를 만들어서 거기에 connects_to database: { writing: :second }를 선언하고 그것을 Item이 상속받는게 좀 더 자연스러운 방법이다.

  • second_record.rb
    class SecondRecord < ApplicationRecord
    self.abstract_class = true
    connects_to database: { writing: :second }
    end
    
  • item.rb
    class Item < SecondRecord
    belongs_to :user, optional: true
    end
    

이제 여기서 main schema에 item을 만들려면 먼저 만들었던 second.item의 model명을 수정하거나 main.item의 model명을 다르게 해야 한다. 그냥 main.item을 만드는것을 시도하면 오류가 발생한다.

rails generate model item name:string price:integer user:references

The name ‘Item’ is either already used in your application or reserved by Ruby on Rails. Please choose an alternative or use –force to skip this check and run this generator again.

3. Second Schema를 별도 Namespace로 분리

second.item을 Second::Item으로 Model명을 변경해보자. 이건 Advanced Rails model generators 에서 rails g model admin/user 에서 생성되는 module 및 파일 구조를 활용하였다.

  • app/models/second.rb
    module Second
    def self.table_name_prefix
      db_name = 'second'
      db_name = "#{db_name}_development" if Rails.env.development?
      db_name = "#{db_name}_test" if Rails.env.test?
      "#{db_name}."
    end
    end
    
  • app/models/item.rb -> app/models/second/item.rb
    class Second::Item < ApplicationRecord
    belongs_to :user, optional: true
    end
    

여기서 눈여겨 봐야할 것은 Item은 다시 ApplicationRecord을 상속받았다. 즉 main의 connection을 사용하면서 development 기준으로 second_development.items로 접근을 한다. 기존에 만들어 두었던 User 모델에 has_many :second_items, class_name: 'Second::Item' 와 같이 선언을 하면 user.second_items 식으로 사용이 가능해진다.

4. main.items 생성 및 Migration 동작 확인

이제 main.items 생성이 가능해졌다.

rails generate model item name:string p:integer user:references
rails db:migrate

second.items와 구분하기 위해서 일부러 컬럼명을 하나 다르게 하였다. 이건 정상동작한다. 이제 main.items 와 second.items에 각각 서로 다른 컬름을 추가해보자. migration이 정상동작하는지 확인하기 위해서다.

먼저 second.items에 option이라는 컬름을 추가해보자.

rails g migration add_option_to_item --database second
  • db/second_migrate/xxxxxxxxxxxxxx_add_option_to_item.rb
class AddOptionToItem < ActiveRecord::Migration[6.0]
  def change
    add_column :items, :option, :string, default: 'none'
  end
end
rails db:migrate

다음으로 main.items에 category라는 컬럼을 추가해보자.

rails g migration add_category_to_item
  • db/migrate/xxxxxxxxxxxxxx_add_category_to_item.rb
class AddCategoryToItem < ActiveRecord::Migration[6.0]
  def change
    add_column :items, :categoty, :string, default: 'kit', index: true
  end
end
rails db:migrate

이러면 각각 다른 모양의 Item 모델로 수정된 것을 확인 가능하다.

  • app/models/item.rb
# == Schema Information
#
# Table name: items
#
#  id         :bigint           not null, primary key
#  categoty   :string(255)      default("kit")
#  name       :string(255)
#  p          :integer
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  user_id    :bigint           not null
#
# Indexes
#
#  index_items_on_user_id  (user_id)
#
# Foreign Keys
#
#  fk_rails_...  (user_id => users.id)
#
class Item < ApplicationRecord
  belongs_to :user
end
  • app/models/second/item.rb
# == Schema Information
#
# Table name: second_development.items
#
#  id         :bigint           not null, primary key
#  name       :string(255)
#  option     :string(255)      default("none")
#  price      :integer
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  user_id    :bigint
#
class Second::Item < ApplicationRecord
  belongs_to :user, optional: true
end

마치며…

아직은 이렇게 정상동작 하는것 까지 확인했다. 실제로 서버를 이렇게 개발하여 운영할때 어떤 문제가 발생할지에 대해서는 잘 그려지지 않는다. 지금으로서는 예상되는 것들에 대해서 다 테스트 했다고 생각하고 있다. 추가적인 내용이 발생하면 이 글을 수정하도록 하겠다.

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)