Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

증가하는 사용자 테이블을 길들이는 방법

고객은 어떻게 그곳에 도착했습니까?

세부 사항을 살펴보기 전에 앱이 어떻게 이 상태가 되는지 이해해 보겠습니다. 간단한 users로 시작합니다. 테이블. 몇 주 후에 마지막 로그인 시간을 결정할 수 있어야 users.last_sign_in_at를 추가할 수 있습니다. . 그런 다음 사용자의 이름을 알아야 합니다. first_name을(를) 추가합니다. 및 last_name . 트위터 핸들? 또 다른 칼럼. GitHub 프로필? 전화 번호? 몇 개월 후 테이블은 상상을 초월할 정도로 변합니다.

이게 무슨 문제야?

큰 표는 몇 가지 문제를 나타냅니다.

  1. users 관련 없는 여러 책임이 있습니다. 이것은 이해, 변경 및 테스트를 더 어렵게 만듭니다.
  2. 앱과 데이터베이스 간에 데이터를 교환하려면 추가 대역폭이 필요합니다.
  3. 앱이 부피가 큰 모델을 저장하려면 더 많은 메모리가 필요합니다.

앱이 User를 가져왔습니다. 인증 및 권한 부여 목적을 위한 모든 요청에 ​​대해 일반적으로 소수의 열만 사용합니다. 문제를 수정하면 디자인과 성능이 모두 향상됩니다.

표 추출

거의 사용하지 않는 열을 새 테이블로 추출하여 문제를 해결할 수 있습니다. . 예를 들어 프로필 정보(first_name 등)을 profiles로 다음 단계에 따라:

  1. profiles 만들기 users의 프로필 관련 열을 복제하는 열 포함 .
  2. profile_id 추가 users에게 . NULL로 설정 지금은.
  3. users의 각 행에 대해 , profiles에 행 삽입 프로필 관련 열을 복제합니다.
  4. 포인트 profile_id users의 해당 행 3에 삽입된 행으로.
  5. 하지 마세요 users.profile_id를 만듭니다. NULL이 아닌 . 앱이 아직 존재를 인식하지 못하므로 중단됩니다.

users.first_name에 대한 참조를 교체해야 합니다. profiles.first_name 포함 등등. 소수의 참조로 몇 개의 열만 추출하는 경우 수동으로 수행하는 것이 좋습니다. 하지만 "아, 안돼. 이건 최악의 직업이야!"라고 생각하는 순간 대안을 찾아야 합니다.

문제를 무시하지 마십시오. 모든 사람이 피하는 코드의 일부는 더 악화되고 더 큰 부주의로 고통받을 것입니다. . 악순환을 끊는 가장 쉬운 방법은 작게 시작하는 것입니다.

내 고객이 문제를 어떻게 해결했는지 궁금하다면 계속 읽어보세요.

한 번에 한 줄씩 코드 수정

가장 점진적인 접근 방식은 한 번에 하나의 참조를 이전 열에 고정하는 것입니다. first_name 이동에 집중합시다. users profiles로 .

먼저 profiles을 생성합니다. 함께:

rails generate model Profile first_name:string

그런 다음 users의 참조를 추가합니다. profilesusers.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로 . 내 고객은 몇 가지 요구 사항을 가지고 있었습니다.

  1. 접근자는 연결된 profiles을 사용해야 합니다. . 또한 사용되지 않는 접근자가 호출된 위치를 기록해야 합니다.
  2. users 저장 자동으로 profiles을 저장해야 합니다. 더 이상 사용되지 않는 접근자를 사용하여 코드가 깨지는 것을 방지하기 위해.
  3. 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 쿼리를 처리하지 않습니다.
  • 원숭이 패치를 깨뜨릴 수 있습니다(종속성이 패치를 도입할 수도 있음을 기억하십시오).
  • usersprofiles profiles.first_name인 경우 동기화되지 않을 수 있습니다. 업데이트되었지만 users.first_name 아닙니다.

당신은 그들 중 일부를 극복 할 수 있습니다. 예를 들어, 서비스 개체 또는 profiles의 콜백과 동기화된 모델을 유지할 수 있습니다. . 또는 PostgreSQL을 사용하는 경우 중간에 구체화된 뷰 사용을 고려할 수 있습니다.

바로 그것입니다!

이 기사의 가장 중요한 교훈은 냄새가 나는 코드를 피하지 말고 직접 해결한다는 것입니다. . 작업이 압도적인 경우 정기적인 일정에 따라 반복적으로 작업하십시오. 제시된 기사 a 테이블을 추출할 때 고려해야 할 방법은 어렵습니다. 적용할 수 없으면 다른 것을 찾으십시오. 방법을 모른다면 저에게 한 줄만 남겨주세요. 도와드리겠습니다. 비트가 썩지 않도록 하십시오.