고객은 어떻게 그곳에 도착했습니까?
세부 사항을 살펴보기 전에 앱이 어떻게 이 상태가 되는지 이해해 보겠습니다. 간단한 users
로 시작합니다. 테이블. 몇 주 후에 마지막 로그인 시간을 결정할 수 있어야 users.last_sign_in_at
를 추가할 수 있습니다. . 그런 다음 사용자의 이름을 알아야 합니다. first_name
을(를) 추가합니다. 및 last_name
. 트위터 핸들? 또 다른 칼럼. GitHub 프로필? 전화 번호? 몇 개월 후 테이블은 상상을 초월할 정도로 변합니다.
이게 무슨 문제야?
큰 표는 몇 가지 문제를 나타냅니다.
users
관련 없는 여러 책임이 있습니다. 이것은 이해, 변경 및 테스트를 더 어렵게 만듭니다.- 앱과 데이터베이스 간에 데이터를 교환하려면 추가 대역폭이 필요합니다.
- 앱이 부피가 큰 모델을 저장하려면 더 많은 메모리가 필요합니다.
앱이 User
를 가져왔습니다. 인증 및 권한 부여 목적을 위한 모든 요청에 대해 일반적으로 소수의 열만 사용합니다. 문제를 수정하면 디자인과 성능이 모두 향상됩니다.
표 추출
거의 사용하지 않는 열을 새 테이블로 추출하여 문제를 해결할 수 있습니다. . 예를 들어 프로필 정보(first_name
등)을 profiles
로 다음 단계에 따라:
profiles
만들기users
의 프로필 관련 열을 복제하는 열 포함 .profile_id
추가users
에게 .NULL
로 설정 지금은.users
의 각 행에 대해 ,profiles
에 행 삽입 프로필 관련 열을 복제합니다.- 포인트
profile_id
users
의 해당 행 3에 삽입된 행으로. - 하지 마세요
users.profile_id
를 만듭니다.NULL
이 아닌 . 앱이 아직 존재를 인식하지 못하므로 중단됩니다.
users.first_name
에 대한 참조를 교체해야 합니다. profiles.first_name
포함 등등. 소수의 참조로 몇 개의 열만 추출하는 경우 수동으로 수행하는 것이 좋습니다. 하지만 "아, 안돼. 이건 최악의 직업이야!"라고 생각하는 순간 대안을 찾아야 합니다.
문제를 무시하지 마십시오. 모든 사람이 피하는 코드의 일부는 더 악화되고 더 큰 부주의로 고통받을 것입니다. . 악순환을 끊는 가장 쉬운 방법은 작게 시작하는 것입니다.
내 고객이 문제를 어떻게 해결했는지 궁금하다면 계속 읽어보세요.
한 번에 한 줄씩 코드 수정
가장 점진적인 접근 방식은 한 번에 하나의 참조를 이전 열에 고정하는 것입니다. first_name
이동에 집중합시다. users
profiles
로 .
먼저 profiles
을 생성합니다. 함께:
rails generate model Profile first_name:string
그런 다음 users
의 참조를 추가합니다. profiles
로 users.first_name
복사 profiles
로 :
class ExtractUsersFirstNameToProfiles < ActiveRecord::Migration
# Redefine the models to break dependency on production code. We need
# vanilla models without callbacks, etc. Also, removing a model in the future
# might break the migration.
class User < ActiveRecord::Base; end
class Profile < ActiveRecord::Base; end
def up
add_reference :users, :profile, index: true, unique: true, foreign_key: true
User.find_each do |user|
profile = Profile.create!(first_name: user.first_name)
user.update!(profile_id: profile.id)
end
change_column_null :users, :profile_id, false
end
def down
remove_reference :users, :profile
end
end
각 사용자에게 정확히 하나의 프로필이 있어야 하기 때문에 users
의 참조 profiles
로 반대 참조보다 바람직합니다.
데이터베이스 구조가 준비되면 first_name
을(를) 위임할 수 있습니다. users
profiles
로 . 내 고객은 몇 가지 요구 사항을 가지고 있었습니다.
- 접근자는 연결된
profiles
을 사용해야 합니다. . 또한 사용되지 않는 접근자가 호출된 위치를 기록해야 합니다. users
저장 자동으로profiles
을 저장해야 합니다. 더 이상 사용되지 않는 접근자를 사용하여 코드가 깨지는 것을 방지하기 위해.User#first_name_changed?
및 기타ActiveModel::Dirty
방법은 여전히 작동합니다.
이것은 users
를 의미합니다. 다음과 같아야 합니다.
class User < ActiveRecord::Base
# We need autosave as the client code might be unaware of
# Profile#first_name and still reference User#first_name.
belongs_to :profile, autosave: true
def first_name
log_backtrace(:first_name)
profile.first_name
end
def first_name=(new_first_name)
log_backtrace(:first_name)
# Call super so that User#first_name_changed? and similar still work as
# expected.
super
profile.first_name = new_first_name
end
private
def log_backtrace(name)
filtered_backtrace = caller.select do |item|
item.start_with?(Rails.root.to_s)
end
Rails.logger.warn(<<-END)
A reference to an obsolete attribute #{name} at:
#{filtered_backtrace.join("\n")}
END
end
end
이러한 변경 후에 앱은 동일하게 작동하지만 profiles
에 대한 추가 참조로 인해 약간 느려질 수 있습니다. (성능이 문제가 되는 경우 AppSignal과 같은 도구를 사용하십시오.) 코드는 수정 불가능한 속성을 포함하여 레거시 속성에 대한 모든 참조를 기록합니다(예:user[attr] = ...
또는 user.send("#{attr}=", ...)
) 그래서 grep
도움이 되지 않습니다.
이 인프라를 사용하면 users.first_name
에 대한 참조 하나를 수정하는 데 전념할 수 있습니다. 정기적인 일정으로, 예를 들어 매일 아침(빠른 승리로 하루를 시작하기 위해) 또는 정오쯤(집중적인 아침 후에 더 쉬운 일을 하기 위해). 이러한 약속은 문제 해결에 대한 정신적 장벽을 줄이는 것이 목표이므로 필수적입니다. . 조치를 취하지 않고 위의 코드를 그대로 두면 앱이 더욱 악화됩니다.
더 이상 사용되지 않는 참조를 모두 제거한 후(grep
로 확인) 및 로그) 마침내 users.first_name
을 삭제할 수 있습니다. :
class RemoveUsersFirstName < ActiveRecord::Migration
def change
remove_column :users, :first_name, :string
end
end
User
에 추가된 코드도 제거해야 합니다. 더 이상 필요하지 않기 때문입니다.
제한 사항
이 방법은 귀하의 사례에 적용될 수 있지만 몇 가지 제한 사항을 염두에 두십시오.
User.update_all
과 같은 대량 쿼리는 처리하지 않습니다. .- 원시 SQL 쿼리를 처리하지 않습니다.
- 원숭이 패치를 깨뜨릴 수 있습니다(종속성이 패치를 도입할 수도 있음을 기억하십시오).
users
및profiles
profiles.first_name
인 경우 동기화되지 않을 수 있습니다. 업데이트되었지만users.first_name
아닙니다.
당신은 그들 중 일부를 극복 할 수 있습니다. 예를 들어, 서비스 개체 또는 profiles
의 콜백과 동기화된 모델을 유지할 수 있습니다. . 또는 PostgreSQL을 사용하는 경우 중간에 구체화된 뷰 사용을 고려할 수 있습니다.
바로 그것입니다!
이 기사의 가장 중요한 교훈은 냄새가 나는 코드를 피하지 말고 직접 해결한다는 것입니다. . 작업이 압도적인 경우 정기적인 일정에 따라 반복적으로 작업하십시오. 제시된 기사 a 테이블을 추출할 때 고려해야 할 방법은 어렵습니다. 적용할 수 없으면 다른 것을 찾으십시오. 방법을 모른다면 저에게 한 줄만 남겨주세요. 도와드리겠습니다. 비트가 썩지 않도록 하십시오.