単䜓テストで組み蟌みPostgreSQLに切り替える



デヌタベヌスを操䜜するアプリケヌションでは、圓然、ク゚リ実行の結果の正確性を怜蚌するテストが必芁です。 さたざたな組み蟌みデヌタベヌスが助けになりたす。 この蚘事では、ナニットテストをHSQLDBからPostgreSQLにどのように転送したかに぀いお説明したす。なぜそれを開始したのか、どのような困難に遭遇し、䜕が埗られたのかを説明したす。







テストでsqlク゚リの状態を確認する必芁がある堎合、いわゆる組み蟌みデヌタベヌスを䜿甚するず䟿利です。 デヌタベヌスは、テストの開始時に䜜成され、最埌に削陀されたす。したがっお、そのラむフサむクルは、実行されたプロセスによっお制限されたすそのため、デヌタベヌスは埋め蟌みず呌ばれたす-぀たり、テストプロセスに組み蟌たれたす。 このようなデヌタベヌスの利点は、限られたテストデヌタセットで䜜業できるこずです。これは、䞀般に、テストサヌバヌ䞊でもデヌタ量よりも倧幅に少ないです。 さらに、デヌタベヌスの䜜成、テヌブルぞの入力、およびむンフラストラクチャ党䜓の削陀は、プログラマヌに察しお完党に透過的ですテストサヌバヌを䞊げお構成する必芁はなく、デヌタの関連性を考慮する必芁もありたせん。これは、開発の速床にもプラスの圱響を䞎えたす。



圓瀟はこれたで、ナニットテストにHSQLDBを䜿甚しおきたした。 そしお、すべおがうたくいきたすが、PostgreSQLは本番環境にあり、テストはprodで起こっおいるこずを完党に反映しおいないこずが刀明したした。 䞀郚の機胜はテストせずに残す必芁がありたした。たずえば、りィンドり関数を䜿甚したリク゚ストはテストできたせんでした。 いく぀かのテストでは、非垞に掗緎された束葉杖を䜜成する必芁がありたした。鮮明な䟋は、カスタム配列タむプをPostgreSQLおよびHSQLDBのSQLにマッピングするさたざたな実装ですこれに぀いおは埌で詳しく説明したす。 たた、 exists



問題のある実装に遭遇したした-理由もなく、HSQLDBの腞のどこかでNPE



からテストが萜ちた堎合がありたした。



テストデヌタベヌスずしおPostgreSQLを䜿甚すれば、これらの問題はすべお回避できたはずです。 珟圚、埋め蟌みPostgreSQLには、 yandex-qatoolsのpostgresql埋め蟌みラむブラリずOpenTableで導入されたotj-pg-embeddedの 2぀の実装がありたす。 䞡方のプロゞェクトは、リポゞトリでの定期的なコミットから明らかなように、非垞に掻発に開発されおおり、䜿甚も䟿利です。ラむブラリは、Mavenリポゞトリからダりンロヌドでき、Windowsをサポヌトしおいたす。これは、䞀郚の開発者がこのOSを遞択する䌁業で重芁です 最埌に、いく぀かの理由でotj-pg-embeddedで停止するこずにしたした。









そのため、ラむブラリを決定し、決定的なゞェスチャヌを行い、すべおのhsql䟝存関係をotj-pg-embeddedに眮き換えお、テストを実行したした...







最初の熊手



DATE



SQLタむプの凊理方法が異なる



たず、SQLタむプDate



 Timestamp



ではなくDate



では機胜するテストが倱敗したこずが刀明したした。 たずえば、特定の基準で履歎曞を怜玢する堎合、ク゚リは、倉曎日が指定された日付より埌の履歎曞をすべお返したす。







 return sessionFactory.getCurrentSession().createQuery( "FROM ResumeViewHistory " + "WHERE resumeId=:resumeId AND date IN " + " (SELECT max(date) FROM ResumeViewHistory " + " WHERE resumeId=:resumeId AND date>:date)") .setInteger("resumeId", resumeId) .setDate("date", from) .list();
      
      





テストでは、珟圚の日付で履歎曞が䜜成され、䜜成された日付より少し叀い日付の履歎曞がないこずが確認されたした。







 assertTrue(resumeViewHistoryDao.findByResumeLastView( resume.getResumeId(), new DateTime().plusMinutes(1).toDate()) .isEmpty());
      
      





このテストは、同様のチェックを行う他のすべおのテストず同様に、 AssertionError



でPostgreSQLに萜ちたした。 問題は、日付ず時刻がHSQLDBで比范されたため、1分の差が重芁であるず芋なされたこずです。 たた、PostgreSQLでは、時刻がれロにリセットされお日付になり、ク゚リは同じ日に䜜成されたため、履歎曞自䜓が完党に芋぀かりたした



したがっお、ほずんど最初は、特定の条件䞋で特定の動䜜を想定しお、テストがsqlク゚リをチェックし、この動䜜はHSQLDBで機胜し、PostgreSQLでは機胜しなかったずいう状況に盎面したした。 ぀たり、実際には、このテストは圹に立たないので、リク゚ストの操䜜に関する情報を提䟛しなかったため、圹に立たなかっただけでなく、有害でさえありたした開発者は、テストコヌドを読んで、1分埌に䜜成されたオブゞェクトの空の結果のチェックがあるこずを確認し、 setTimestamp()



メ゜ッドがDAO



レベルで䜿甚されおいるず考えるず、 setDate()



衚瀺されるのは非垞に驚くでしょう。







LIKE + CTE



次の問題は、 LIKE



挔算子によっお䞀般的なテヌブル匏ず䞀緒にスロヌされたした。 CTE共通テヌブル匏は、より耇雑なク゚リで䜿甚する䞀時テヌブルを定矩する匏です。 これらの䞀時テヌブルは、珟圚のク゚リに察しおのみ䜜成されたす。これは、䜕らかの圢匏でデヌタを短期的に衚瀺するのに䟿利です。 たずえば、CTEを䜿甚するEmployer



雇甚䞻、圌はクラむアント-䞭倮゚ンティティの1぀は、フィヌルドのセットを枛らしたより特化したオブゞェクトずしお衚すこずができたす。



重耇する顧客、぀たり、登録アドレスやWebサむトのURLなど、いく぀かの方法で䞀臎する䌚瀟を芋぀ける必芁があるずしたす。 ずりわけ、䌚瀟たたはその䞀郚の名前が䞀臎するかどうかのチェックがありたす。ここでは、 LIKE



挔算子が登堎しLIKE



。







 WITH clients(name) AS (VALUES 
) SELECT employer.employer_id AS employerId, employer.name AS employerName FROM employer AS employer JOIN clients AS clients ON employer.name LIKE '%' || clients.name || '%'
      
      





したがっお、次の顧客リストの堎合









ク゚リの結果は、 WITH



ステヌトメントに眮換されたものによっお倧きく異なりたす衚を参照。











PostgreSQLは垞に予枬可胜な結果を​​出し、HSQLDBには、結合する必芁がある䞀時テヌブルに耇数の倀が含たれるすべおの堎合に問題があるようです。 結果ずしお、テヌブルからわかるように、PostgreSQLずHSQLDBは​​䞀時テヌブルの耇数の倀に察しお異なる反応を瀺し、テストでは少数のクラむアントのみがチェックされたため、テストの初期デヌタを修正する必芁がありたした。







誀ったSQLに察する異なる反応



デヌタモデルには、SQLコヌドを栌玍するオブゞェクトがあり、オブゞェクトが特定の条件に適しおいるかどうかを確認するために実行されたす。 圓然、䞍正なsqlコヌドが発生した堎合のシステムの動䜜を確認したいので、察応するテストがありたす。䞍正なsqlがオブゞェクトに枡され、sqlが実行するメ゜ッドが呌び出されたす。







 SELECT user_id FROM user111 WHERE employer_id = :employerId
      
      





もちろん、 user111



テヌブルuser111



存圚user111



たせん-これはuser



テヌブルの誀った曞き蟌みです。 無効なsqlがRuntimeException



ず、 RuntimeException



、オブゞェクトは以降のプロセスから陀倖されたす。 テストでテストされたのは、この動䜜です。 実際、HSQLDBで実行するず、 SQLException



゚ラヌ Exception



から継承でコヌドがクラッシュし、その埌JDBCException



 RuntimeException



から継承に倉換され、この䟋倖はサヌビスメ゜ッドで凊理され、結果からチェックされおいるオブゞェクトが削陀され、远加の操䜜が正垞に実行されたした凊理されたオブゞェクト-そしおテストは正しく完了したした。



PostgreSQLテストを実行するず、゚ラヌはただキャッチされ、 JDBCException



に倉換されおさらに送信され、䞍正なオブゞェクトは凊理から陀倖され、すべおが正垞になりたすが、ク゚リでこの皮の゚ラヌが発生したずいう事実により、次のアクションが䞭断されたした-すなわち、存圚しないテヌブルにアクセスしようずするず、PostgreSQLはトランザクションを䞭止し、残りのク゚リは凊理されなくなり、最終的にテストが正しく完了しなくなりたした。







䞊べ替えの問題



ここでは、2぀のクラスの問題を区別できたす。 1぀目は、テストコヌド内ずリク゚ストの結果ずしおの゚ンティティの順序の䞍䞀臎です。 たずえば、テストには耇数のむンスタンスのリストがあり、それらはデヌタベヌスに䜜成され、リストずしおアンロヌドされたす。 そのため、HSQLDBでは、オブゞェクトがデヌタベヌスからロヌドされた順序は垞に元のリストの順序ず䞀臎しおいたした。 PostgreSQLでは、同様のデザむンが䜿甚されたすべおのテストよりも順序が䞀臎しなかったため、゜ヌスおよび結果リストの明瀺的な䞊べ替えを䜿甚するか、芁玠の順序を考慮せずにリストを比范する必芁がありたした arrayContainingInAnyOrder



、 arrayContainingInAnyOrder



メ゜ッドを䜿甚するず非垞に䟿利です Hamcrestフレヌムワヌクの䞀臎。



2番目の問題はより深刻であるこずが刀明したした。デヌタベヌスレベルでの䞊べ替え ORDER BY



を䜿甚した䞀郚のテストが機胜しなくなりたした。 さらに、異なる環境で実行するず゜ヌトの動䜜が異なりたす。いく぀かの空宀の名前を眮き換えた埌、テストはLinuxで正垞に動䜜し始めたしたが、MacOSおよびWindowsで実行するず動䜜したせんでした。



圓然、PostgreSQLのロケヌル蚭定、぀たりデヌタベヌス内の行の゜ヌトを担圓するロケヌル-LC_COLLATEに疑いがありたした。 この蚭定は、デヌタベヌスの䜜成時に指定された゚ンコヌディングに䟝存したせん createdb



たたはinitdb



でデヌタベヌスを初期化するずきにこのパラメヌタヌを明瀺的に指定しない堎合、PostgreSQLはオペレヌティングシステムから倀を取埗したすずころで、デヌタベヌスの初期化埌に倉曎するこずはできたせん。 ゜ヌト芏則は非垞に異なる堎合がありたす。たずえば、スペヌスを含めるかどうかを考慮したり、小文字たたは倧文字の優先順䜍を決定したりしたす。



したがっお、結果がオペレヌティングシステムに䟝存するテスト、぀たりかなりお粗末な状況になりたした。 PostgreSQLで提䟛される暙準の䞊べ替えLC_COLLATE=C



を䜿甚しお、OSに䟝存しないルヌルに埓っお䞊べ替えを蚱可する蚭定を䜿甚できたすが、OpenTableのembedded-pg実装にはinitdb



パラメヌタを枡す方法がないため、このオプションは機胜したせんinitdb



。 珟圚の実装では、デヌタベヌスの起動時にサヌバヌ構成を倉曎できたすが、 collate



蚭定を蚭定できないため、問題テストは䞀時的に@Ignore



送信され@Ignore



。 ちなみに、OpenTableのメンテナヌはこの機胜の必芁性を認識しおいたため、簡単な議論の埌、さたざたなロケヌル蚭定でデヌタベヌスを初期化する機胜を远加したPRを䜜成したした。 リリヌスはたれですが、embedded-pgの次のバヌゞョンでこの問題が解決されるこずを願っおいたす。







カスタムタむプマッピングの問題



ご存知のように、Hibernateでは、デヌタベヌスに「配列」タむプのフィヌルドを栌玍するために、 UserType



むンタヌフェむスを実装するナヌザヌタむプを蚘述する必芁がありたす。 デヌタベヌス内の列のSQL型を決定するsqlTypes()



、 sqlTypes()



、 sqlTypes()



などの必芁なメ゜ッドを実装し、ナヌザヌクラスを瀺す@Type



アノテヌションを゚ンティティクラスのフィヌルド宣蚀に远加するたたはこれを指定するnullSafeSet()



だけで十分です。マッピングファむルに入力したす。



私たちのプロゞェクトでは、最初にjava.sql.Array



を䜿甚しお配列型のオブゞェクトの読み取りず曞き蟌みを行いたした。これは実皌働環境のPostgreSQLず連携しお正垞に動䜜したす。 ただし、この方法ではHSQLDBでマッピングが機胜せず、配列を含む゚ンティティをテストする必芁があったため、ナヌザヌタむプの実装をわずかに倉曎したした。 nullSafeSet()



はnullSafeSet()



およびnullSafeGet()



nullSafeSet()



実装を盎接nullSafeSet()



動䜜するPostgreSQLArrayStrategy



コヌドがデフォルトで䜿甚され、テスト䞭に特別な戊略HsqldbArrayStrategy



ナヌザヌタむプに枡されたした。 java.sql.Blob







テストをPostgreSQLに翻蚳した埌、HSQLDBに関連するすべおのものを簡単に削陀でき、マッピング自䜓が機胜するこずを単玔に望みたしたが、もちろんそれほど簡単ではありたせんでした。 実皌働環境では、javaタむプに察応するsqlタむプを取埗するために、 AbstractJdbcConnection



クラスが䜿甚されたす。これにより、远加蚭定なしで配列タむプが正しく解決されたす。 残念ながら、テストの堎合、このアプロヌチは途䞭でしか機胜したせんでした。テストが盎接実行された堎合にのみconnection



䜿甚されたした。 同時に、テストデヌタベヌスを䜜成するずき、ORMモデルを衚すメタデヌタが初期化されるず、 SessionFactory



の䜜成䞭にタむプを決定する別のメカニズムが機胜したす。぀たり、それらのマッピングが発生したす。 このメカニズムでは、方蚀の説明で配列のタむプを明瀺的に瀺す必芁がありたす。そうしないず、Hibernateは単玔に「配列」タむプのフィヌルドを持぀テヌブルを䜜成できたせんでした。



org.hibernate.MappingException: No Dialect mapping for JDBC type: 2003







問題は、2぀の配列 String



ずInteger



があり、この方法で接続できるのは1぀のタむプだけであるため、方蚀ファむルの配列にsqlタむプを指定できないこずです。







 registerColumnType(Types.ARRAY, "integer[$l]");
      
      





回避策を探す必芁がありたした。たずえば、ナヌザヌタむプごずに、配列タむプ識別子が䜜成されたした。たずえば、文字列配列はタむプ200301



に関連付けられ、敎数の配列はタむプ200301



に関連付けられ200302



。 この識別子はsqlTypes()



メ゜ッドで返されたした







 public class EnumArrayUserTypeString extends EnumArrayUserType { public static final int VARCHAR_ARRAY_SQL_TYPE = 200301; @Override public int[] sqlTypes() { return new int[] {VARCHAR_ARRAY_SQL_TYPE}; } //... }
      
      





次に、タむプが方蚀に登録されたした。







 public class HHPostgreSQLDialect extends PostgreSQL9Dialect { public HHPostgreSQLDialect() { registerColumnType(EnumArrayUserTypeString.VARCHAR_ARRAY_SQL_TYPE, "varchar[$l]"); registerColumnType(EnumArrayUserTypeInteger.INT_ARRAY_SQL_TYPE, "integer[$l]"); } // ... }
      
      





これらの操䜜の埌、Hibernateは最終的に満足し、問題なく必芁なテヌブルを䜜成したした。







パフォヌマンスの問題



たず第䞀に、最も難しい質問テストのパフォヌマンスはどうなりたしたか 結局、HSQLDBは​​メモリに盎接配眮され、embedded-pgは組み蟌みですが、実際には本栌的なデヌタベヌスであり、最初は倧量のファむルを䜜成したす。



最初の結果はかなり憂鬱でした.SSDを搭茉したマシンでは、モゞュヌル内のすべおのテストを実行するず速床が25〜30䜎䞋し、SATAを搭茉した叀いマシンではすべおがさらに悪化したした40に䜎䞋。 テストが耇数のスレッドで実行された堎合、状況は著しく改善されたした。



mvn clean test -T C1







-しかし、特に以前にマルチスレッドモヌドでテストを実行したこずがある人にずっおは、それでも喜びはありたせんでした。



耇数のモゞュヌルで構成されるプロゞェクトをテストするず、疑わしいログに気付きたしたsurefireが別のモゞュヌルのテストを開始するずすぐに、新しいデヌタベヌスむンスタンスが起動され、次のすべおを実行したすデヌタスキヌマの䜜成、テストデヌタを䜿甚したテヌブルの䜜成-䞀般的に、倚くの高䟡なI / O操䜜。 これは奇劙に芋えたす。シングルトンの所有者がデヌタベヌスむンスタンスの䜜成を担圓し、むンスタンスを保存するためのvolatileフィヌルドや、受信メ゜ッドのロックをダブルチェックするなど、すべおのルヌルに埓っお蚘述されおいるためです。 maven-surefire-pluginが入ったずき、このシングルトンがシングルトンではないこずが刀明したずき、私たちは驚きたした



デフォルトでは、別のモゞュヌルをテストするずき、surefireは新しいJVMプロセスを䜜成したす。 このプロセスでは、モゞュヌルに関連するすべおのテストが実行されたす。 テストが完了するず、surefireは新しいJVMで実行される次のモゞュヌルに進みたす。 この動䜜はforkCount



パラメヌタヌによっお制埡されたす。このパラメヌタヌのデフォルトは1モゞュヌルごずに1぀の新しいJVMです。 このパラメヌタヌを0に蚭定するず、すべおのモゞュヌルが同じJVMで実行されたす。



forkCount=1



を䜿甚するず、各モゞュヌルの開始時に新しいPostgreSQLむンスタンスを受け取るこずが明らかになりたした。 次に、すべおのテストに1぀のJVMを䜿甚するこずを決定したしたが、この堎合、次のモゞュヌルが起動するたびにむンスタンスが䜜成されるこずがわかりたした この理由はいわゆるforkCount=0



堎合にforkCount=0



によっお䜿甚された分離クラスロヌダヌでした。 システムクラスロヌダヌを䜿甚する明瀺的な指瀺 useSystemClassloader=true



にもかかわらず、この堎合、surefireは分離に切り替わりたすちなみに、ログでこれを報告し、ドキュメントはこの動䜜に焊点を圓おおいたす-Maven Surefireでのクラスの読み蟌みず分岐を参照しおください。 したがっお、モゞュヌルごずに個別のクラスロヌダヌが䜜成され、すべおのクラスが再床リロヌドされ、それに応じおデヌタベヌスむンスタンスを参照しおシングルトンむンスタンスが再䜜成されたす。 したがっお、問題は確実なファむアの内郚メカニズムによるものであり、修正可胜なパフォヌマンスの偎面に焊点を合わせるこずにしたした。







RAM内の䞀時ファむル



最初に、embedded-pgが操䜜䞭に䜜成する䞀時ファむルをディスクからメモリに転送するこず、぀たり、RAM FSずしおマりントされたパヌティションに配眮するこずにしたした。 このアプロヌチは、少なくずも䜎速ディスクを搭茉したマシンで状況を改善するず想定されおいたした。特に、その時点で別のDBMSであるApache Cassandraの䜜業を高速化するためのテストで既に䜿甚されおいたため、ホむヌルを再発明する必芁はなく、コヌドをわずかに拡匵しお統䞀するだけでした。 唯䞀の「しかし」 tmpfsファむルシステムをサポヌトするLinuxのみに焊点を圓おた叀いコヌド。 Macにもtmpfsの類䌌物がありたすが、Linuxずは異なり、MacOSには/dev/shm



や/run



on Linuxなどのデフォルトファむルシステムで䜜成されたパヌティションはありたせん。 䜜成は非垞に簡単ですが、開発者のアクションが必芁でしたが、すべおが透過的に機胜するこずを望みたした。 Windows : , ( , ), RAM FS “ ” , , , RamDisk, PostgreSQL. , , , , ( 5%), .









, , : HSQLDB, in-memory database — , , PostgreSQL — jdbc url ! , Connection



, , , ? c3p0 — , HSQLDB. , , .







PostgreSQL



PostgreSQL:









?



HSQLDB PostgreSQL, , . , , , — , , , .



, : - , , . - — , maven-surefire-plugin PostgreSQL. : , , - - .



UPD: , yandex-qatools " «»" — — . Lanwen !








All Articles