改行コードから学んだ意図しないデータが生成されないようにする工夫

この記事はANDPAD Advent Calendar 2023 23日目の記事です。

こんにちは、motokikando です!
アンドパッドに1期生で新卒で入社してから早いもので、9ヶ月が経ちました! 7月からプロダクトチームの1つであるANDPAD黒板チームにjoinし 、 現在サーバーサイドをメインに業務をしています。

今回はその中で起こった、見た目上、同じ2つの黒板ができてしまうという謎の現象を解消し、そこから学んだ知見についてご紹介します!

改行コードとは?


本題に入る前に改行コードの種類についてご紹介します。

改行コードは文字通り、テキスト内で改行を表すための特殊な文字コードのことです。下記の LF, CR, CRLF の3種類とそれぞれの特徴が存在します。

  • LF : \n
    • 正式名称: Line Feed ラインフィード
    • 実行内容: カーソルを新しい行に移動させる↓
    • 対応OS: Linux、macOS(Mac OS X以降)など
  • CR : \r
    • 正式名称: Carriage Return キャリッジ・リターン
    • 実行内容: カーソルを行の先頭に戻す ←
    • 対応OS: MacOS 9以前で利用 *1
  • CR + LF : \r\n
    • 正式名称: Carriage Return キャリッジ・リターン + Line Feed ラインフィード
    • 実行内容: カーソルを行の先頭に戻す + 新しい行に移動させる ↵
    • 対応OS: Microsoft Windows など

これらは、上記の OS に加え、プロトコルやファイルフォーマットなどの条件によって違いが見られます。

事例: 改行コードで重複したドメインオブジェクトが作成できてしまうという問題


次に、この改行コードによって発生した問題と、チームがとった対応について説明します。

ANDPAD黒板は現状 Nuxt.js + Rails で実装されており、黒板というドメインオブジェクトを保有しています。

この時、改行コードが \n\r\n の2種類が、図のようにリクエストする際のcontent-typeの違いによって異なる改行コードが、複数のリレーションを持つ黒板のドメインオブジェクトに含まれてしまっていました。*2

content-typeの違いによって複数の改行コードが紛れ込んでしまった図

そして、具体的には

  1. \r\nがMySQLで2文字判定されてしまい、黒板作成時の文字列長のバリデーションに意図せず引っかかる
  2. 黒板の重複は許容しないが、改行コード(\r\nまたは\n)の違いによって、見た目上は同じ2つの黒板ができてしまう

といった問題が発生していました。

1. \r\n がMySQLで2文字判定されてしまい、黒板作成時のvalidationに意図せず引っかかる

2. 黒板の重複は許容しないが、\r\nまたは\nの違いによって、見た目上は同じ2つの黒板ができてしまう

そして、今後黒板の数が案件単位で増えていく中で、改行コードによる意図しない挙動や重複が発生することを防ぐことや、今後改行コードを懸念する必要がないようにという認知負荷を下げる目的で

  • 文字数制限時のバリデーションをするときには、 \nであること
  • 黒板の重複判定をするときには、 \nであること
  • データベースに保存されるときには、 \nであること

の3つの対応を行うこととなりました。

まとめると、黒板に含まれる改行コードは全て1文字判定にして、今後作成されるものは\nに統一しよう! ということになります。

対応を通して学んだ、意図しないデータが生成されないようにする工夫


ここからは、改行コードのリファクタリング対応の取り組みやレビューを通して学んだ、Railsの意図しないデータが生成され、DBに値が保存されないようにする工夫についてご紹介します !

フロントエンド、Model、Controllerのバリデーション、DBの制約で整合性をとる

バリデーションや制約と一言に言っても、

  • フロントエンド側のバリデーション
  • バックエンド側
    • Controller(またはServiceクラス)でのバリデーション
    • Modelでのバリデーション
  • DBの制約

などそれぞれにどのような役割を持ってバリデーションを行うべきなのか、役割を明確にして対応することが必要であることを学びました。

各バリデーションの図解

フロントエンド側のバリデーションについて

フロントエンド側のバリデーションでは、イベント発生時にバリデーションによる表示を行うことで、UXの向上を目指すという目的を持ちます。

例えば、

  • フォーム入力中にリアルタイムで入力の妥当性を確認する
  • パスワード入力中に、強力なパスワードの要件を提示する

などです。つまり、バックエンド側にリクエストを送る前にフィードバックを送ることで、段階的に修正を行えるようにすることが大切であると学びました。

また、改行コードではフロントエンド側とバックエンド側のバリデーションの差異によるバグも生まれたこともあったため、双方で認識を統一しておく必要があると感じました。

バックエンド側のバリデーションについて

基本的には、Model層で主に行い、セキュリティやデータベースに登録するデータの抜け道を無くすようにするという目的があります。

具体的な方法だと

  • セキュリティ強化:ユーザー入力に対する検証・サニタイズをしておく
  • データの整合性:外部キーと必須フィールドの確認により、データの一貫性と完全性を維持する

などがあります。

Controller側(もしくはServiceクラス)でバリデーションも行うこともできますが、あるドメインオブジェクトを作成する際にあるControllerの中で必ず、バリデーションを実施している必要があり、Controller内で定義漏れがあった場合はバリデーションをすり抜けてしまうという問題が起きる可能性があります。

そのため、Controller側(もしくはServiceクラス)であるドメインオブジェクトの重複が起きないことを担保しながらも、DBに保存を行う前にModel層で、バリデーションの制約をかけておくということが重要であることを学びました。

DBの制約について

現場で役立つシステム設計の原則によると、DBに値が保存される値は、

  • NOT NULL制約で、コトの起きた事実を確実に記録する
  • 一意性制約で、データの重複やあいまいさに苦しまないようにする
  • 外部キー制約で、事実として正しく記録された複数の関係を明確にする

が大切であると述べています。

最終的なデータベースの管理のコツとして、業務アプリケーションでは、コトを正しく記録し、最後の制約として、なるべく考慮事項を減らして参照できるようにすることが大切であることを学びました。

まとめると、今回の改行コード対応を通して、データフローの中でフロントエンドからDBまでを一気通貫したデータの整合性を考慮することで認知負荷を減らすことができ、とりわけフロントエンドではUI/UXを意識するバリデーションを、そしてModel層・DBではデータの一貫性を確実に担保できる制約を設ける必要性を学びました。

複数のドメインオブジェクトにまたがるオブジェクトの重複判定は共通して1つのものを使う

ANDPAD黒板における「黒板」というオブジェクトは、見た目上は1つのテーブルのデータに見えますが、

  • 複数のテーブルのリレーションを持っている
  • つまり、ドメインオブジェクトも複数の関連を持つ
  • テンプレートによって形式が異なる
  • 付随して階層形式の別のツリーデータも生成される

など、黒板が重複していないことを明確にするには、ロジックが増えていくほど複雑になるという課題があります。

複数の箇所で重複判定を用意すると、仕様が複雑なのが相まって不整合が生まれやすくなるため、それぞれの箇所で判定ロジックを持つのではなく、判定ロジックは1つに統一し、それを各箇所で利用することを徹底することの重要さを学びました。

コールバックを上手く活用する、でも信用しすぎない

今回のリファクタリングの取り組みとして、DBに保存される改行コードは必ず \n に統一して保存されるようにするという対応を行いました。

class SampleModel < ApplicationRecord
  before_validation :normalize 
  private
  def normalize
    self.name = name&.gsub(/\R/, "\n")
  end
end 

コールバックは適切なオブジェクトのライフサイクルのタイミングで実施してくれるため便利ですが、コールバックが実行されないケースもありました。

例えば、 あるデータの一括登録時には注意が必要です。 Railsには insert_all や gemの activerecord-import など、データを一括でDBに保存する機能があり、それぞれ以下のような特徴があります。

  • insert_all
    • 実行時にActiveRecord のvalidationが実施されない: 参考
  • activerecord_import
    • [ Hash, Hash, ... ] などデータ形式の場合は、validation が実施されない : 参考

などの問題が発生し、 before_validation 呼んでおけば必ずcreate 実行タイミングで、改行コードを置換してくれるし、DBには \r\n から\nに置換されているだろうという期待を見事に打ち砕かれました。

この部分についてはコールバックを上手く活用しながらも、確実に実行されるのかを自動テスト等などを使って確認することが大切だと学びました。

まとめ


本記事では、改行コードから学んだ意図しないデータが生成されないようにする工夫について紹介を行いました。

ユーザーの目には見えない内部の地道な作業ではありましたが、この対応を通して多くの学びがあり、データ量が増えていくプロダクトに対して認知負荷や意図しない不具合を減らすという意味で有意義な取り組みだったのではないかと思っています。

今後もプロダクトを成長させるための地道な改善や、開発を続けていけたらと思っています!

アンドパッドでは、「幸せを築く人を、幸せに。」というミッションの実現のため、一緒に働く仲間を大募集しています。 会社や事業、開発チームにご興味を持たれた方は、下記のサイトをぜひご覧ください。

engineer.andpad.co.jp

*1:現在ではCR単体ではほぼ使われていないようです。

*2:詳細な原因としては、黒板に豆図という画像をつけるために、multipart/form-dataを使って、複数の画像タイプもリクエストに含められるようにという方針になっていたのですが、この時に改行コードが \r\nになるという挙動になっていました。参考