Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Sansanアーキテクチャ史 / A Brief History of Sansan's Ar...

Sansan
October 23, 2019

Sansanアーキテクチャ史 / A Brief History of Sansan's Architecture

■イベント
Sansan Builders Box 2019
https://jp.corp-sansan.com/sbb2019/

■登壇概要 

タイトル:Sansanアーキテクチャ史

登壇者: 
プロダクト開発部 荒川聖悟

▼Sansan Builders Box 

https://buildersbox.corp-sansan.com/

Sansan

October 23, 2019
Tweet

More Decks by Sansan

Other Decks in Technology

Transcript

  1. Sansan Builders Box Sansanとは? AI×人によるデータ入力 データベース化 マルチデバイスで活用 他システムとの連携 お客様 AIなどのテクノロジーとオペレーターに

    よる多重入力を活用し、高い入力精度を実現 名刺をスキャン 4 3 2 1 Sansanスキャナ スマートフォンアプリ
  2. v1

  3. Sansan Builders Box v1 : コードの雰囲気( BLL ) BLL DAL

    WebForms // CardInformationBll.cs public void DeleteCard(string cardId) { try { connLK.OpenConnection(); connLK.BeginTransaction(); CardInformationDal dal = new CardInformationDal(); DataTable dt = dal.GetCard(cardId); if (dal.DeleteStack(dt.Rows[0]) { throw new NotExpectedResultException(message); } connLK.Commit(); // ロールバック処理などは省略 } }
  4. Sansan Builders Box v1 : コードの雰囲気( BLL ) BLL DAL

    WebForms // CardInformationBll.cs public void DeleteBizCard(string cardId) { try { connLK.OpenConnection(); connLK.BeginTransaction(); CardInformationDal dal = new CardInformationDal(); DataTable dt = dal.GetCard(cardId); if (dal.DeleteStack(dt.Rows[0]) { throw new NotExpectedResultException(message); } connLK.Commit(); // ロールバック処理などは省略 } } トランザクション管理
  5. Sansan Builders Box v1 : コードの雰囲気( BLL ) BLL DAL

    WebForms // CardInformationBll.cs public void DeleteBizCard(string cardId) { try { connLK.OpenConnection(); connLK.BeginTransaction(); CardInformationDal dal = new CardInformationDal(); DataTable dt = dal.GetCard(cardId); if (dal.DeleteStack(dt.Rows[0]) { throw new NotExpectedResultException(message); } connLK.Commit(); // ロールバック処理などは省略 } } DALと1対1で対応
  6. Sansan Builders Box v1 : コードの雰囲気( DAL ) BLL DAL

    WebForms // CardInformationDal.cs public void DeleteStack(string cardId) { var sql = new StringBuilder(); sql.AppendFormat("update {0} c", Const.TableName); sql.AppendFormat(" set delete_flag = :{0}", Const.Delete); sql.AppendFormat(" where c.{0} = :{1}", Const.CardId, cardId); return DBHelper.ExecuteQuery(sql.ToString()); } 手動でクエリ結合
  7. v2

  8. Sansan Builders Box v2 : 概要 Domain Model Repository Service

    ビジネスロジックを担当するレイヤ モデルとO/Rマッピングを担当するレイヤ データアクセスの抽象化レイヤ
  9. Sansan Builders Box v2 : 概要 Domain Model Repository Service

    ビジネスロジックを担当するレイヤ モデルとO/Rマッピングを担当するレイヤ データアクセスの抽象化レイヤ 用語的には いわゆるDDDライクな アーキテクチャ
  10. Sansan Builders Box v2 : アーキテクチャ図 ICardDeleteService (Interface) Service Domain

    Model Repository CardDelete Service Card CardRepository ICardRepository (Interface)
  11. Sansan Builders Box v2 : コードの雰囲気(Service) Domain Model Repository Service

    // CardDeleteServcie.cs public CardDeleteResult Delete(string cardId) { try { var target = StackRepository .Where((stack, card) => card.Id == cardId) .Select((stack, card) => new object[] { cardId }) .SingleOrDefault(); Transaction.Begin(); target.Delete(); CardRepository.Save(new[] { target }); Transaction.Commit(); return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc cess, Target = target }; } }
  12. Sansan Builders Box v2 : コードの雰囲気(Service) Domain Model Repository Service

    // CardDeleteServcie.cs public CardDeleteResult Delete(string cardId) { try { var target = CardRepository .Where((stack, card) => card.Id == cardId) .Select((stack, card) => new object[] { cardId }) .SingleOrDefault(); Transaction.Begin(); target.Delete(); CardRepository.Save(new[] { target }); Transaction.Commit(); return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc cess, Target = target }; } } O/R マッピング
  13. Sansan Builders Box v2 : コードの雰囲気(Service) Domain Model Repository Service

    // CardDeleteServcie.cs public CardDeleteResult Delete(string cardId) { try { var target = CardRepository .Where((stack, card) => card.Id == cardId) .Select((stack, card) => new object[] { cardId }) .SingleOrDefault(); Transaction.Begin(); target.Delete(); CardRepository.Save(new[] { target }); Transaction.Commit(); return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc cess, Target = target }; } } Repositoryが抽象化!
  14. Sansan Builders Box v2 : コードの雰囲気(Domain Model) Domain Model Repository

    Service // Card.cs [TableName("trn_card")] public class Card { [ColumnName("id", ColumnType.VarChar, 10)] public string Id { get; set; } ... } テーブル名を指定 カラムの情報を指定
  15. Sansan Builders Box v2 : コードの雰囲気(Repository) Domain Model Repository Service

    foreach (var custom in CustomUpdate) { if (custom is string) { var customString = (string)custom; switch (customString) { case "Address.PostalCode": addressSetClause.AppendFormat("{0}postal_code = :POSTAL_CODE ", comma); var postalCodeParam = DbParameterFactory.Create(card.Address.PostalCode); postalCodeParam.ParameterName = ":POSTAL_CODE"; paramList.Add(postalCodeParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Prefecture": addressSetClause.AppendFormat("{0}address_prefecture = :ADDRESS_PREFECTURE ", comma); var prefectureParam = DbParameterFactory.Create(card.Address.Prefecture); prefectureParam.ParameterName = ":ADDRESS_PREFECTURE"; paramList.Add(prefectureParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.City": addressSetClause.AppendFormat("{0}address_city = :ADDRESS_CITY ", comma); var cityParam = DbParameterFactory.Create(card.Address.City); cityParam.ParameterName = ":ADDRESS_CITY"; paramList.Add(cityParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Street": addressSetClause.AppendFormat("{0}address_street = :ADDRESS_STREET ", comma); var streetParam = DbParameterFactory.Create(card.Address.Street); streetParam.ParameterName = ":ADDRESS_STREET"; paramList.Add(streetParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Building": addressSetClause.AppendFormat("{0}address_building = :ADDRESS_BUILDING ", comma); var buildingParam = DbParameterFactory.Create(card.Address.Building); buildingParam.ParameterName = ":ADDRESS_BUILDING";
  16. Sansan Builders Box v2 : コードの雰囲気(Repository) Domain Model Repository Service

    foreach (var custom in CustomUpdate) { if (custom is string) { var customString = (string)custom; switch (customString) { case "Address.PostalCode": addressSetClause.AppendFormat("{0}postal_code = :POSTAL_CODE ", comma); var postalCodeParam = DbParameterFactory.Create(card.Address.PostalCode); postalCodeParam.ParameterName = ":POSTAL_CODE"; paramList.Add(postalCodeParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Prefecture": addressSetClause.AppendFormat("{0}address_prefecture = :ADDRESS_PREFECTURE ", comma); var prefectureParam = DbParameterFactory.Create(card.Address.Prefecture); prefectureParam.ParameterName = ":ADDRESS_PREFECTURE"; paramList.Add(prefectureParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.City": addressSetClause.AppendFormat("{0}address_city = :ADDRESS_CITY ", comma); var cityParam = DbParameterFactory.Create(card.Address.City); cityParam.ParameterName = ":ADDRESS_CITY"; paramList.Add(cityParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Street": addressSetClause.AppendFormat("{0}address_street = :ADDRESS_STREET ", comma); var streetParam = DbParameterFactory.Create(card.Address.Street); streetParam.ParameterName = ":ADDRESS_STREET"; paramList.Add(streetParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Building": addressSetClause.AppendFormat("{0}address_building = :ADDRESS_BUILDING ", comma); var buildingParam = DbParameterFactory.Create(card.Address.Building); buildingParam.ParameterName = ":ADDRESS_BUILDING"; リポジトリが”超”肥大化 ※ 1メソッド内の処理
  17. Sansan Builders Box v2の課題と学び - Repositoryの肥大化と実装難易度の高さ - Domain ModelごとにRepositoryができる -

    一応レイヤごとに分割したが、業務ごとに分割できずに、肥大化する - 「共通化」の認識を共通化させることが出来なかった - フロントエンド固有のロジックが共通されたりする(実はAPI専用メソッドなど) - エンジニア全員に思想と実装を浸透させることはさらに難しい - とにかく遅い!パフォーマンス出ない! - 発行されるクエリが多い&謎すぎてチューニング作業が難航
  18. v3

  19. Sansan Builders Box v3 : アーキテクチャ図 UI Domain Controller Application

    CardDelete Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure
  20. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure フロントエンド固有のクエリを発行 (基本SELECT文のみ) UI
  21. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure 1対1で対応 UI
  22. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure ①共通のクエリを発行する UI
  23. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure ②テクノロジ依存を隠蔽化 UI
  24. Sansan Builders Box v3 : コードの雰囲気( Application ) Domain Infrastructure

    Application // BizCardService.cs public DeleteResult DeleteCard(string cardId) { var card = CardDataAccessor.GetDeleteTargetCard(cardId); if (card == null) { return DeleteResult.NotFound; } using (var tx = TransactionScopeFactory.Create()) { CardDataAccessor.DeleteStack(card.Id); tx.Complete(); } return DeleteResult.Success; }
  25. Sansan Builders Box v3 : コードの雰囲気( Application ) Domain Infrastructure

    Application // BizCardService.cs public DeleteResult DeleteCard(string cardId) { var card = CardDataAccessor.GetDeleteTargetCard(cardId); if (card == null) { return DeleteResult.NotFound; } using (var tx = TransactionScopeFactory.Create()) { CardDataAccessor.DeleteStack(card.Id); tx.Complete(); } return DeleteResult.Success; } トランザクション管理
  26. Sansan Builders Box v3 : コードの雰囲気( Domain ) Domain Infrastructure

    Application // IBizCardDataAccessor.cs public interface IBizCardDataAccessor { void DeleteCard(string cardId) } インターフェースを切る
  27. Sansan Builders Box v3 : コードの雰囲気( Infrastructure ) Domain Application

    // BizCardDeleteDataAccessor.cs public void DeleteCard(string cardId) { var sql = @" update lkweb.trn_card set delete_flag = 1 where card_id = @cardId"; using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)) { conn.Execute(sql, new { cardId }); } } Infrastructure
  28. Sansan Builders Box v3 : コードの雰囲気( Infrastructure ) Domain Application

    // BizCardDeleteDataAccessor.cs public void DeleteStack(string cardId) { var sql = @" update lkweb.trn_card set delete_flag = 1 where card_id = @cardId"; using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)) { conn.Execute(sql, new { cardId }); } } SQLを直で指定する Infrastructure
  29. Sansan Builders Box v3 : コードの雰囲気( Infrastructure ) Domain Application

    // BizCardDeleteDataAccessor.cs public void DeleteCard(string cardId) { var sql = @" update lkweb.trn_card set delete_flag = 1 where card_id = @cardId"; using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)) { conn.Execute(sql, new { cardId }); } } 実装クラス Infrastructure
  30. Sansan Builders Box 学びのまとめ - 時代とともに流行のアーキテクチャは移り変わる - いわゆる「レガシー」もこうして生まれる - アーキテクチャの浸透は想像以上に難しい

    - 弊部では新入社員向けにアーキテクチャに関する講座を有志で実施している - プラットフォームとアプリケーションに合ったアーキテクチャを選ぶ