Ruby on Rails One Database Multiple Schema 적용
in Development on Rails
Rails 6 부터 Multiple Database가 지원된다. 그 이전에는 octopus를 이용해서 했지만, 이제는 더 이상 그럴 필요가 없게 되었다.
위 가이드 대로하면 Replica 적용도 쉽게 가능하며, Multiple Schema 적용도 가능하다. Schema 파일과 Migration 파일들을 database 별로 따로 관리가 가능해진다.
이렇게 할 경우 다른 Schema에 있는 같은 이름의 Model을 처리할려면 Model명을 수정해야 한다. 그리고 서로 다른 Schema의 Model들이 하나의 폴더 app/models
에서 관리된다.
애시당초 Multiple Database를 사용하지 않고 Namespace로 나누는 방법도 있다.
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
마치며…
아직은 이렇게 정상동작 하는것 까지 확인했다. 실제로 서버를 이렇게 개발하여 운영할때 어떤 문제가 발생할지에 대해서는 잘 그려지지 않는다. 지금으로서는 예상되는 것들에 대해서 다 테스트 했다고 생각하고 있다. 추가적인 내용이 발생하면 이 글을 수정하도록 하겠다.
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)