때때로 우리가 통제할 수 없는 독특한 상황과 상황이 매우 비정통적인 요구 사항으로 이어집니다. 최근에 어떤 레코드에도 데이터베이스 ID에 의존하지 않고 ActiveRecord를 사용해야 했던 경험이 있습니다. 누군가가 같은 작업을 고려하고 있다면 다른 방법을 찾는 것이 좋습니다! 하지만 나머지 이야기로 넘어가겠습니다.
결정이 내려졌다. 더 작은 데이터베이스(구조에는 클론이지만 데이터에는 없음)를 병합해야 했습니다. 팀이 한 데이터베이스에서 다른 데이터베이스로 데이터베이스 레코드를 복사하여 붙여넣는 스크립트를 마무리하는 동안 프로젝트에 합류했습니다. ID를 포함하여 모든 것을 있는 그대로 복사했습니다.
데이터베이스 A
id | 과일 | user_id |
---|---|---|
... | ... | ... |
123 | 주황색 | 456 |
... | ... | ... |
데이터베이스 B
id | 과일 | user_id |
---|---|---|
... | ... | ... |
123 | 바나나 | 74 |
... | ... | ... |
병합 후 데이터베이스 A
id | 과일 | user_id |
---|---|---|
... | ... | ... |
123 | 주황색 | 456 |
123 | 바나나 | 74 |
... | ... | ... |
이것은 ID를 갖는 근본적인 이유인 고유 식별을 깨뜨립니다. 구체적인 내용은 몰랐는데 시스템에 중복 ID가 도입되면 온갖 문제가 생길 것 같았다. 나는 무언가를 말하려고 했지만 나는 이 프로젝트를 처음 접했고 다른 사람들은 이것이 최선의 길이라고 확신하는 것 같았습니다. 며칠 안에 코드를 배포하고 중복 ID가 있는 데이터를 처리하기 시작했습니다. 질문은 더 이상 "우리가 이것을해야합니까?"가 아닙니다. 대신에 "이를 어떻게 합니까?"라는 질문이 있었습니다. "얼마나 더 걸릴까요?"
중복 ID로 작업
그렇다면 중복 ID가 있는 데이터는 어떻게 처리합니까? 해결책은 여러 필드의 복합 ID를 만드는 것이었습니다. 대부분의 DB 가져오기는 다음과 같습니다.
# This doesn't work, there may be 2 users with id: 123
FavoriteFruit.find(123)
# Multiple IDs scope the query to the correct record
FavoriteFruit.find_by(id: 123, user_id: 456)
모든 ActiveRecord 호출은 이러한 방식으로 업데이트되었으며 코드를 살펴보니 이해가 되는 것 같았습니다. 배포할 때까지.
모든 지옥이 풀린다
코드를 배포한 직후 전화가 울리기 시작했습니다. 고객은 합산되지 않은 숫자를 보고 있었습니다. 그들은 자신의 기록을 업데이트할 수 없었습니다. 모든 종류의 기능이 중단되었습니다.
우리는 무엇을 해야 합니까? 우리는 단순히 코드를 배포하지 않았습니다. 또한 한 데이터베이스에서 다른 데이터베이스로 데이터를 이동했습니다(배포 후 새 데이터가 생성/업데이트됨). 단순한 롤백 상황이 아니었다. 문제를 빨리 해결해야 했습니다.
Rails는 무엇을 하고 있나요?
디버깅의 첫 번째 단계는 현재 동작이 무엇인지와 오류를 재현하는 방법을 확인하는 것이었습니다. 프로덕션 데이터의 복제본을 가져와 Rails 콘솔을 시작했습니다. 설정에 따라 ActiveRecord 쿼리를 실행할 때 Rails가 실행하는 SQL 쿼리가 자동으로 표시되지 않을 수 있습니다. SQL 문이 콘솔에 표시되도록 하는 방법은 다음과 같습니다.
ActiveRecord::Base.logger = Logger.new(STDOUT)
그 후 몇 가지 일반적인 Rails 쿼리를 시도했습니다.
$ FavoriteFruit.find_by(id: 123, user_id: 456)
FavoriteFruit Load (0.6ms)
SELECT "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]
find_by
잘 작동하는 것 같았지만 다음과 같은 코드를 보았습니다.
fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
...
...
fruit.reload
reload
궁금해서 저도 테스트해봤습니다:
$ fruit.reload
FavoriteFruit Load (0.3ms)
SELECT "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
LIMIT $2
[["id", 123], ["LIMIT", 1]]
어 오. 따라서 처음에 find_by
로 올바른 레코드를 가져왔음에도 불구하고 , reload
를 호출할 때마다 , 레코드의 ID를 사용하여 간단한 ID로 찾기 쿼리를 수행합니다. 물론 중복 ID로 인해 종종 잘못된 데이터를 제공합니다.
왜 그랬을까? Rails 소스 코드에서 단서를 찾았습니다. 이것은 Ruby on Rails를 사용한 코딩의 훌륭한 측면이며, 소스 코드는 일반 Ruby이며 자유롭게 액세스할 수 있습니다. 간단히 "ActiveRecord reload"를 검색하여 빠르게 찾았습니다.
# File activerecord/lib/active_record/persistence.rb, line 602
def reload(options = nil)
self.class.connection.clear_query_cache
fresh_object =
if options && options[:lock]
self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
@attributes = fresh_object.instance_variable_get("@attributes")
@new_record = false
self
end
이것은 reload
self.class.find(id)
에 대한 래퍼입니다. . ID로만 쿼리하는 것은 이 방법에 내장되어 있습니다. 중복 ID로 작업하려면 핵심 Rails 메서드를 재정의하거나(절대 권장하지 않음) reload
사용을 중지해야 합니다. 완전히.
우리의 솔루션
그래서 우리는 모든 reload
코드에서 find_by
로 변경 여러 키를 통해 데이터베이스를 가져옵니다.
그러나 이는 일부 버그만 해결되었습니다. 더 파고든 후 update
를 테스트하기로 결정했습니다. 통화:
$ fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
$ fruit.update(last_eaten: Time.now)
FavoriteFruit Update (43.3ms)
UPDATE "favorite_fruits"
SET "last_eaten" = $1
WHERE "favorite_fruits"."id" = $2
[["updated_at", "2020-04-16 06:24:57.989195"], ["id", 123]]
어 오. find_by
update
를 호출할 때 특정 필드로 레코드 범위를 지정했습니다. Rails 레코드에서 간단한 WHERE id = x
를 생성했습니다. 중복 ID로도 중단되는 쿼리입니다. 이 문제를 어떻게 해결했습니까?
사용자 정의 업데이트 방법인 update_unique
를 만들었습니다. , 다음과 같습니다.
class FavoriteFruit
def update_unique(attributes)
run_callbacks :save do
self.class
.where(id: id, user_id: user_id)
.update_all(attributes)
end
self.class.find_by(id: id, user_id: user_id)
end
end
ID 이상으로 범위가 지정된 레코드를 업데이트할 수 있습니다.
$ fruit.update_unique(last_eaten: Time.now)
FavoriteFruit Update All (3.2ms)
UPDATE "favorite_fruits"
SET "last_eaten" = '2020-04-16 06:24:57.989195'
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]
이 코드는 레코드 업데이트를 위한 좁은 범위를 보장했지만 클래스의 update_all
메서드에서 일반적으로 레코드 업데이트와 함께 제공되는 콜백을 잃어버렸습니다. 따라서 update_all
이후 업데이트된 레코드를 검색하기 위해 수동으로 콜백을 실행하고 다른 데이터베이스 호출을 수행해야 했습니다. 업데이트된 레코드를 반환하지 않습니다. 최종 제품은 너무 아닙니다. 지저분하지만 확실히 fruit.update
보다 읽기 어렵습니다. .
실제 솔루션
비용, 관리 및 시간 제약으로 인해 우리의 솔루션은 모든 데이터베이스 호출에 대해 여러 키를 사용하도록 Rails를 패치하는 것이 었습니다. 이것은 고객이 여전히 제품을 구매하고 사용할 것이라는 점에서 효과가 있었지만 다음과 같은 몇 가지 이유로 잘못된 생각이었습니다.
- 향후 개발에서는 일반적인 Rails 방법을 사용하여 실수로 버그를 다시 도입할 수 있습니다. 신규 개발자는
reload
사용과 같이 숨겨진 버그가 없는 코드를 유지하기 위해 엄격한 교육이 필요합니다. 방법. - 코드가 더 복잡하고 명확하지 않으며 유지 관리가 덜 쉽습니다. 이것은 프로젝트가 진행되면서 점점 더 개발 속도를 늦추는 기술적 부채입니다.
- 테스트 속도가 많이 느려집니다. 기능이 작동하는지 뿐만 아니라 다양한 객체에 중복된 ID가 있을 때 작동하는지 테스트해야 합니다. 테스트를 작성하는 데 더 많은 시간이 걸리고 테스트 스위트가 실행될 때마다 모든 추가 테스트를 실행하는 데 더 많은 시간이 걸립니다. 또한 프로젝트의 각 개발자가 가능한 모든 시나리오를 주의 깊게 테스트하지 않으면 테스트에서 버그를 쉽게 놓칠 수 있습니다.
이 문제에 대한 실제 해결책은 처음부터 중복 ID를 사용하지 않는 것입니다. 데이터를 한 데이터베이스에서 다른 데이터베이스로 전송해야 하는 경우 이를 수행하는 스크립트는 ID 없이 데이터를 수집하고 삽입하여 수신 데이터베이스가 표준화된 자동 증가 카운터를 사용하여 각 레코드에 고유한 ID를 부여할 수 있도록 해야 합니다.
또 다른 솔루션은 모든 레코드에 UUID를 사용하는 것입니다. 이 유형의 ID는 무작위로 생성된 긴 문자열입니다(정수 ID에서와 같이 단계별 계산 대신). 그러면 데이터를 다른 데이터베이스로 이동해도 충돌이나 문제가 발생하지 않습니다.
결론은 Rails는 ID가 레코드별로 고유하다는 것을 이해하고 데이터베이스의 특정 데이터를 빠르고 쉽게 조작할 수 있는 방법으로 구축되었다는 것입니다. Rails는 독단적인 프레임워크이며, 이것의 장점은 Rails의 작업 방식을 고수하는 한 모든 것이 얼마나 원활하게 실행되는지입니다. 이것은 Rails뿐만 아니라 프로그래밍의 다른 많은 측면에도 적용됩니다. 상황이 복잡해지면 문제를 식별하는 방법을 알아야 합니다. 그러나 명확하고 유지 관리 가능하며 일반적인 코드를 작성하면 이러한 복잡한 문제를 처음부터 피할 수 있습니다.