Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

開発

春なのでSpringへの移行ガイド

こんにちは、開発ユニットbcです。

みなさんご存知かとは思いますが、Seasar2のサポートが2016年9月26日を以って終了します。
弊社の運営するSUPERDELIVERYでも一部の機能でSeasar2を現在も利用しています。

今回はSeasar2からの移行を検討している方の参考になればと思い、以前に行った移行の事例を紹介させていただきます。

2014年にSUPERDELIVERYの商品登録機能をリニューアルした際、弊社ではSeasar2からSpring Framework(4系)へ移行することを選択しました。
選択の理由はプロダクトの開発が停滞していないことと、Seasar2からの移行のしやすさでした。
なぜ移行がしやすいかというと、Seasar2の各機能を代替する機能がSpring Frameworkにも備わっているからです。

ではまずはSeasar2とSpring Frameworkの構成の比較から。

構成の比較

移行前 移行後
Controller S2Struts Spring MVC
DI Seasar2 Spring Framework
DAO S2Dao Spring Data

このようにSeasar2の機能の移行先がSpring Frameworkにもあることが分かります。
では、商品一覧表示の機能を例にしてどのように移行するかを見ていきましょう。

移行前と移行後の実装を比較

移行前のシステムでは、コンポーネント毎に設定ファイルへ記述するスタイルで開発していたので、
新たに機能の追加がある場合は以下のファイルの追加が必要でした。
  1. XXXAction.java
  2. XXXActionImpl.java
  3. XXXService.java
  4. XXXServiceImpl.java
  5. XXXDao.java
  6. XXX.dicon
  7. struts-config-XXX.xml
■ProductSearchAction.java
public interface ProductSearchAction {
  String execute();
}
■ProductSearchActionImpl.java
public class ProductSearchActionImpl implements ProductSearchAction {
  private ProductSearchService productSearchService;
  private List<Product> products;
  private ProductSearchForm searchForm;
	
  public String execute() {
  products = productSearchService.findProducts(searchForm.createProductSearchCondition());
  return "success";
  }

  public List<Product> getProducts() {
  return products;
  }

  public void setProductSearchService(ProductSearchService productSearchService) {
  this.productSearchService = productSearchService;
  }

  public void setSearchForm(ProductSearchForm searchForm) {
  this.searchForm = searchForm;
  }
}
■ProductSearchService.java
public interface ProductSearchService {
List<Product> findProducts(ProductSearchCondition condition);
}
■ProductSearchServiceImpl.java
public class ProductSearchServiceImpl {
private ProductDao productDao;

public List<Product> findProducts(ProductSearchCondition condition) {
return productDao.findByStatusCode(condition.getStatusCode());
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
}
■ProductDao.java
public interface ProductDAO {
  @Query("status_code = ?")
  List<Product> findByStatusCode(Integer statusCode);
  void insert(Product product);
  void update(Product product);
  void delete(Product product);
}
■action.dicon
<components>
... <component name="productSearchAction" class="jp.ne.raccoon.action.product.impl.ProductSearchActionImpl" instance="request"> </component>
... </components>
■service.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.service.product.impl.ProductSearchServiceImpl">
  </component>
  ...
</components>
■dao.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.dao.ProducDao">
  </component>
  ...
</components>
■struts-config-product.xml
<struts-config>
  <action-mappings>
... <action path="/product/search" type="jp.ne.raccoon.action.product.ProductSearchAction" parameter="execute"> <forward name="success" path="/product/search.html" /> </action>
... </action-mappings> </struts-config>
  1. DIはdiconファイルにコンポーネントを個別に定義して、Seasar2のセッターインジェクションを利用していました。
  2. リクエストのマッピングは、struts-config.xmlにaction-mappingsをaction毎に個別に設定していました。
 
移行後は以下のような実装になりました。
■ProductSearchController.java
@Controller
public class ProductSearchController {
@Autowired private ProductSearchService productSearchService; @RequestMapping(value = "/search", method = RequestMethod.GET) public String execute( @Valid @ModelAttribute("searchForm") ProductSearchForm searchForm, BindingResult bindingResult, Model model) {
model.addAttribute(
"products",
productSearchService.findProducts(searchForm.createProductSearchCondition()));
return "product/search"; } }
■ProductSearchServiceImpl.java (※interfaceは移行前と同じなので割愛してます。)
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
@Autowired private ProductRepository productRepository; public String findProducts(ProductSearchCondition condition) { return productRepository.findByStatusCode(condition.getStatusCode()); } }
■ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
  List<Product> findByStatusCode(Integer statusCode);
}

移行前と比べると以下のように実装を置き換えることができました。
Action → Controller
Service → Service
Dao → Repository
diconファイル → @Controller @Service等のアノテーション
struts-config → @RequstMapping
  1. 設定ファイルを必要とせず、Springのアノテーションで コンポーネントの定義とDIを行うようになりました。
  2.  @RequestMappingを利用することでリクエストメソッドの指定やパラメーターの受け取り方等がより柔軟な設定を書けるようになりました。
  3. Daoのinsert/update/deleteのメソッドは、継承元に定義されているのでメソッドを用意する必要はなく、@Queryに相当する物はfindByXXXの部分の命名規則によってwhere句が生成されます。※where status_code = ? が生成されます。
これだけで、新機能の追加に必要な作業がだいぶ軽減されました。
順調に移行できそうでしたが、問題点もありました。

問題点

一つ目が、sqlファイルの外部化です。
S2Daoの資産として、参照系の動的なsqlは外部ファイル化していました。 
Springのプロダクトでは対応している物が見当たらず、可能ならば慣れ親しんだ形で移行したいという思いもあって、Mirageというライブラリを導入することにしました。 

■ProductDao_findProducts.sql
//日付の範囲指定と商品の状態を検索条件で指定できることを例としてます。
select
  product_code
from
  product
/*BEGIN*/ where /*IF dateFrom != null*/ and created_at >= /*dateFrom*/ /*END*/ /*IF dateTo != null*/ and created_at < /*dateTo*/ + 1 /*END*/ /*IF productStatus !=null*/ and product_status = /*productStatus*/
 /*END*/
/*END*/
■ProductDao.java
@Component
@Transactional(readOnly = true)
public class ProductDaoImpl implements ProductDao {
  @Autowired
  private SqlManager sqlManager;

  public Product findProducts(SearchCondition condition) {
//上記sqlファイルを指定 SqlResource sqlResource = new ClasspathSqlResource("ProductDao_find_products.sql"); return sqlManager.getResultList(Product.class, sqlResource, condition); } }
mirageで実装したDaoも@ComponentでSpringのコンポーネントとして定義しました。
必要に応じてspringDataとmirageを使い分けるようにして、移行前の資産も活かすことができました。

二つ目がSeasar2とSpringでのコンポーネントのライフサイクルの違いでした。
S2Strutsの時は、Actionはリクエスト単位で作成していましたが、SpringMVCのControllerでは、デフォルトがシングルトンなため、リクエストやセッション等の短いライフサイクルのコンポーネントのDIができないということでした。
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST) public class XXXController { }
@Scopeの設定でライフサイクルを変えることで対応はできますが、全部のControllerに書くのは・・・ということで、以下のようなアノテーションを作成してライフサイクルをリクエストにしたいControllerに指定するようにしました。
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public @interface RequestScopeController {
}
利用する側のController
@RequestScopeController
public class XXXController {
}
これで、リクエストやセッションのライフサイクルのコンポーネントをDIしても問題なく動作するようになりました。

まとめ  

局所的ではありますが、以上がSeasar2から移行した時の内容です。いかがだったでしょうか。
Seasar2のサポート終了をきっかけに新しいことに挑戦していきたいですね。

[E2Eテスト]メンテナンス自動化~パスワードの定期変更に対応する~

こんにちは。
技術戦略部のやすだです。

以前弊社のCI/CDへの取組みについての記事がありましたが、
今回の記事はその中で触れられていた「Selenium」を利用したE2Eテストのシナリオメンテナンスについてです。

経緯 

 弊社ではSeleniumを使ったE2Eテストを行っています。
 その中で、ログイン後の機能について確認するシナリオがあり、ログインに利用するテスト用アカウントのパスワードをシナリオに直接記述していました。

 しかし、弊社ではセキュリティの観点から、社内で利用している全てのテストアカウント用のパスワードをバッチで定期的に自動変更しています。
 
 シナリオに記述されているパスワードについては、定期変更がされる度に人力で変更していたため、シナリオ側のパスワードを変更しそびれると、ログインできずにシナリオの実行に失敗することになってしまいます。 
 というわけで、今回の要望です。
 

要望

 パスワードが変更されたタイミングでSeleniumシナリオに記述しているパスワードも自動で変更したい!
 

概要

 まずはざっくりとパスワード更新バッチとSeleniumのシナリオについての図です。
スライド2

 DBに保存されるパスワードはハッシュ化したものですので、Seleniumのシナリオで利用するパスワードは生成した直後の平文である必要があります。
 そのため、下記の2点の対応を行います。
  1.[パスワード更新バッチ]ハッシュ化前のパスワードをファイルに出力する
   Seleniumのシナリオでログイン後ページを確認するためだけのアカウントなので、何か特別なことができるわけではありませんが、 平文のパスワードを保存するのでサーバーのアクセスコントロールは適切に設定する必要があります。
  2.[テストシナリオ]ファイルからパスワードを取得する

 上記の対応を行うことで、このようになります。 
指摘対応中_技術ブログネタ

詳細内容

 ということで詳細内容です。
 1.[パスワード更新バッチ]ハッシュ化前のパスワードをファイルに出力する
   生成した直後のハッシュ化前の平文パスワードをファイルに出力します。
   Seleinumのシナリオ内では、データファイルとしてHTMLファイルが扱いやすくなっています。
   文字列をHTMLで出力するだけで特別なことは行っていないため、内容については割愛しちゃいましょう。
   ファイルは下記のような形にしています。
password.html
<html>
  <head></head>
    <body>
       <div id="password">1234</div>
    </body>
</html>

 2.[テストシナリオ]ファイルからパスワードを取得する
   Seleniumのシナリオ内ではjavascriptを記述することができます。
   さらに、storeEvalを使うことで値を保持できるため、異なるページで取得した値を取り扱うことが可能です。
   シナリオ内でのjavascriptの実行については、下記の要領でタグ内に記述することで実行することができます。
<td>
 javascript{"'" + this.page().getCurrentWindow().document.getElementById("password").textContent.toString() + "'";}
</td>

  実際にパスワードを取得する際の記述は下記のようになります。
  • パスワードファイルを開く
<tr>
    <td>open</td>
    <td>社内サーバーに配置したパスワードファイルのpath</td>
    <td></td>
</tr>
  • パスワードを取得し、storeする
<tr>
<td>storeEval</td>
    <td>javascript{"'" +this.page().getCurrentWindow().document.getElementById("password").textContent.toString() + "'";}</td>
    <td>get_pass</td>
</tr>
  • storeしたパスワードを他のページで使う
    storeEvalで格納した値は他のページに遷移した後も保持されます。
    そのため、異なるページでも下記の要領で使いまわすことが可能です。
<tr>
    <td>open</td>
    <td>http://www.superdelivery.com/</td>
    <td></td>
</tr>
<tr>
    <td>type</td>
    <td>name=password</td>
    <td>${get_pass}</td>
</tr>


まとめ

  E2Eテストを自動化する最大のメリットはリグレッションテストのコストや障害の発生を抑えられることだと思っています。
 リグレッションテストのコストや障害の発生を抑えることは、システム変更を容易にすることにつながります。
 システム変更が容易であれば、より良いサービスをスピーディーにお届けすることができます。
 ですが、E2Eテストを自動化する場合、どうしてもシナリオのメンテナンスコストが発生してしまうのが悩みどころとなります。
 今回のようにメンテナンス自体もなるべく自動化することで、E2Eテストのメンテナンスコストという悩みを解決し、最終的により良いサービスをスピーディーにお届けできればというのが個人的な思いです。

CORECを支える技術 ~ログ監視編~ fluentd-AmazonSNS連携

こんにちは。なべです。
CORECの開発を担当しています。
CORECを支える技術 ~チケット駆動編~』に続き、『CORECを支える技術』シリーズの第2弾としてCORECのログ監視について書きたいと思います。

旧ログ監視の問題と対策

当初、CORECのログ監視はログ集約にくっつける形で始まりました。
ログ集約はログファイルを一定時間ごとにバックアップする仕組みで、各サーバーのログをfluentdで集め、1時間ごとに圧縮しS3に格納しています。
ログ監視は、その集約されたログを社内の監視システムで取得し、エラーログがあった場合にメールとIRCで通知するというものです。

この仕組には大きく以下の2つの問題がありました。

  • タイムラグ
    ログの監視をできるタイミングがS3に圧縮ファイルがアップされた後なので、検知するまで1時間のタイムラグが発生する可能性がある。
  • サービスレベル
    ログ監視を社内のサーバーに配置していて、消防点検や停電または、サーバーメンテナンス時に監視できないなどサービスレベルにも問題がある。

これらの問題に対応するためfluentdに集めたタイミングでエラーログを抽出し、AmazonSNS経由でメール通知することにしました。
これにより、社内のサーバーも経由しなくなり、タイムラグの問題・サービスレベルの問題を同時に解消できました。

CORECログ監視概要図

実施内容

この対応で実施したことは以下の4つです

  1. fluentdにプラグイン追加
  2. AmazonSNSに通知用Topic作成
  3. メールBody整形用のERB追加
  4. fluentdの設定ファイル記述

1. fluentdにプラグイン追加

以下の2つのプラグインを追加しました。

fluent-plugin-sns
Amazon SNS通知用
fluent-plugin-rewrite-tag-filter
エラーのフィルタ・レベルごとの分岐処理用
fluentdはrubyで動いているのでRubyistならお馴染みのgemコマンドでインストールできます。
この時fluentdが使っているrubyの環境を指定する必要があります。
特にfluentdにバンドルされたrubyを使用している場合は注意してください。
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-sns
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-rewrite-tag-filter

2. AmazonSNSに通知用Topic作成

Amazonの管理コンソールからSNSサービスを選択し、画面に従ってTopic及びSubscritionを作成します。 特に迷うことはないかと思いますが、注意するポイントは以下の2点です。

  • メールアドレスを登録するとメールがくるのでクリックして認証すること
  • 作成したTopicの権限設定を確認する
    権限設定とは作成したTopicに対して、通知に利用するAWSアカウントがこのトピックに投稿する権限のことです。もし権限がない場合には、[ポリシーの編集]から許可を与えてください。

3. メールBody整形用のERB追加

CORECの場合、fluentdが受け取るレコードは改行が\nにエスケープされていて読みづらいため、テンプレートで改行コードに変換しました。
以下のコードをerbファイルとして保存しておきます。
これだけのコードですが受け取った側の負担がだいぶ違います。特にスタックトレースで威力を発揮します。

<%record.each do |k,v|%>
<%="#{k}:#{v.to_s.gsub(/\\n/,"\n")}" %>
<%end%>

4. fluentdの設定ファイル記述

それでは最後にメインとなるfluentdの設定ファイルの編集をします。
変更/追加のポイントは以下の3点です。

4-a. copyプラグインで処理を2系統に分岐する
4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する
4-c. snsプラグインで通知するためのmatchディレクティブを追加する

それぞれ簡単に説明します。

4-a. copyプラグインで処理を2系統に分岐する

fluentdは最初にマッチしたmatchディレクティブだけを実行しますので、同じレコードに対して複数の処理をしたい場合にはcopyプラグインを使用します。
※copyプラグインは標準のプラグインなのでインストールをする必要はありません。

matchディレクティブと同等の内容をstoreタグに書くことができ、同じレコードに対して処理を複数記述できるようになります。
今回の対応では従来のS3への送信する処理をcopyで分岐し、SNSに送信する処理を追加します。

<match tag.log>
  type copy
  <store>
        従来の処理
  </store>
  <store>
        追加したい処理
  </store>
</match>

4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する

ただ単に前項の「追加したい処理」にSNS送信のディレクティブの内容を書いてしまうと、アプリケーションが出力したログすべて(エラーでないものも含めて)通知されてしまいます。
そこでログの内容によって処理を振り分けるにはrewrite_tag_filterプラグインを使用します。
レコードのキーを指定して、検索する文字列または正規表現を指定し、それにマッチした場合に新しいタグを指定する仕組みです。
今回はログレベルがFATALもしくはERRORの場合にシステム通知がしたかったので、ログレベルが格納されているキー(lebel)に対して、FATAL・ERRORそれぞれルールを作成しました。
複数のルールを設定するにはrewriteruleの後の数字をインクリメントして記述します。

  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>

4-c. snsプラグインで通知するためのmatchディレクティブを追加する

typeにsnsを指定して、内容にはAWSユーザーにはお馴染みのパラメータ群を設定していきます。 sns_body_templateのパスはtd-agentが参照可能な場所にERBを配置して指定してください。

<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>

fluentd設定ファイルイメージ(全体)

変更前後の設定ファイルは以下のとおりです。
行はだいぶ増えていますが、それぞれの設定内容に難しいところはありませんでした。

  • 設定ファイル:修正前
<match corec.rails>
    S3に送信する設定(省略)
</match>
  • 設定ファイル:修正後
<match corec.rails>
  type copy
  <store>
        従来のS3に送信する設定(省略)
  </store>
  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>
</match>
<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>
<match corec.rails.sns_error>
 (省略)
</match>

設定ファイル修正後、fluentdを再起動すれば反映されます。
※fluentdの連携をしている場合には送信元のfluentdも再起動することをおすすめします。
 私が設定した時には送信側が受信側を見失ってログの送信が止まってしまいました。

新たな問題

前項までの対応により、旧ログ監視の問題は解消し、 リアルタイムで、かつサービスレベルの高い仕組みができました。
が、新たな問題がいくつか発生しました。

  1. フィルタリングが無くなったのでERROR・FATALのレベルのログが全部飛んでくる
  2. 異常が発生するとガンガンメールがくる
  3. IRCに通知されない

新たな問題 1. ERROR・FATALのレベルのログが全部飛んでくる
→ 対策: 随時チケット化してエラーログの根本原因を潰す!

実はログを検索してメールを飛ばす処理の中でホワイトリストでフィルタリングして、通知するものを絞っていました。
その仕組みが無くなったのでERROR・FATALのレベルのログが全部飛んでくるようになってしまいました。 fluentdでフィルタリングするという選択肢もありましたが、対応の必要のないFATALやERRORを出すシステム側を修正すべきということで、チケット化してエラーの根本対応を実施。

新たな問題 2. 異常が発生するとガンガンメールがくる
→ 対策: 覚悟しておく!

従来のログ監視は1時間に1回ログを検索してメールを飛ばす処理だったため過去1時間に何件エラーが発生していても1通の通知にまとまっていました。
それがリアルタイムでエラー1件毎に通知されるため、異常事態にはメールが大量に送信されてしまいます。
実際に外部ベンダー側にトラブルがあり、復旧までに4時間かかった際に断続的に300通送信されてきました。orz
しかし、異常が発生しているのだから通知はあるべきでそれを止めるのは本末転倒ということで受け入れることにしました。

新たな問題 3. IRCに通知されない
→ 対策:社内で導入を進めている新監視システムへ

メールと同時にIRCに通知して、業務時間中はメールよりも気づきやすいように対策していましたが、IRCは社内サーバーで稼働しているため利用できませんでした。
現状は通知のために旧監視システムと並行稼動をしていますが、実際にはリアルタイムに飛んでくるメールに十分に気づけています。
今後は社内で進めているSlackのサービスを利用した障害対応フローに移行する予定です。
AmazonSNSに通知先を増やすだけで対応できるはずです。

まとめ

  • fluentdにプラグインを追加することで簡単にAmazonSNS経由の通知を実現できました。
    こういったプラグインが充実しているのもfluentdの魅力だと思います。(fluentdの導入については『fluentd(td-agent) の導入』を参照してください。)

  • ログ監視も即時性・対障害性の高いシステムを構築して確実で迅速な検知が必要です!
    ログ監視は最後の砦であり将来発生する障害の芽を発見するアンテナだと思っています。これからもCORECとともにCORECを支える運用システムもブラッシュアップしていきます。

  • 無駄なログは技術負債。根本から除去しましょう!
    無駄なログがたくさん通知されてくると、本来拾わなければいけないログを見落としてしまう可能性があります。そのために通知するログを絞ることは大切ですが、安易にフィルタリングをするのではなく、一つ一つ原因を突き止めて対応すべきところはプログラムで対応し、技術負債を残さないようにしたいですね。

[Rails]Model/テーブル設計で必ず覚えておきたいSTI

開発チームの下田です。

Ruby on Railsについて、基本的なこと(modelやcontrollerが何か知っている程度)を理解されている方を対象としています。

BtoBクラウド受注・発注システムCORECはRailsで開発しています。 開発初期のコードを見直してみると失敗したなと思うことがあります。 中でも特に開発効率に影響しているなと思うのが、RailsのActiveRecordのSTI(Single Table Inheritance、単一テーブル継承)を活用しなかったことです。

STIを簡単に言うと、モデルを永続化するときに、どのクラスなのかというメタ情報を含めてデータベースに保存します。 これだけではわかりづらいと思うので、失敗した例と改善例からご紹介したいと思います。

悪い設計

仮に下記の仕様のコードを書くとします。

要件1:
 CORECではWEBアプリから発注書を送信できます。送信方法は次の3つから選べます。

  • WEBアプリ内で送信する
  • メールで送信する
  • FAXで送信する

この時、次のようなコードにしていました。
(項目名、メソッド名は意味が通じるように和名にしています)

# 注文書model 
class Order < ActiveRecord::Base
  include FAX送信モジュール
  validates :送信方法, :presence => true
 
  # WEBで送信するなら送信先IDが必須 
  validates :送信先ユーザ_id, :presence => true, if: ->{ 送信方法 == 'WEB' }
 
  # メールならメールアドレスが必須 
  validates :メールアドレス, :presence => true, if: ->{ 送信方法 == 'メール' }
 
  # FAXならFAX番号が必須 
  validates :FAX番号, :presence => true, if: ->{ 送信方法 == 'FAX' }
 
  has_many :FAX送信結果
 
  def 送信する
    self.送信済 = true
    validate!
    case 送信方法
    when 'メール'
      注文Mailer.メール注文書送信.deliver
    when 'FAX'
      FAX送信する
    end
    save
  end
 
  private
 
  def FAX送信する
    FAX送信結果.create(送信結果: FAX送信モジュール.送信)
  end
end


何が問題か

当初は送信するときだから、送信手段ごとに処理をcase-whenで振り分けることに違和感はありませんでした。しかし、開発を進めていると、同じコードがあちこちに現れて読みづらいコードになり始めました。

要件2:

  • FAXは送信に失敗することがあるので、結果判定をする
  • WEBとメールは必ず成功したとみなす
class Order < ActiveRecord::Base
  # ~~ 略 ~~ 
 
  def 送信成功?
    # メソッドごとに送信方法による分岐がある状態 
    case 送信方法
    when 'FAX'
      FAX送信結果.送信成功?
    default
      true
    end
  end
end


あるべき姿

FAXもメールもすべて注文書の情報なのでOrderクラスにしたことは正しいのですが、送信方法によってふるまいが違います。役割が同じでふるまいが違うなら、サブクラスを作りポリモーフィズムを持たせるべきです。

sti


class Order < ActiveRecord::Base
  def 送信する
    self.送信済 = true
    save
  end
 
  def 送信成功?
    false
  end
end
 
# WEBで送信する注文書クラス
class Order::Web < Order
  validates :送信先ユーザ_id, :presence => true
 
  def 送信する
    validate!
    super
  end
end
 
# メールで送信する注文書クラス
class Order::Mail < Order
  validates :メールアドレス, :presence => true
 
  def 送信する
    validate!
    注文Mailer.メール注文書送信.deliver
    super
  end
end
 
# FAXで送信する注文書クラス 
class Order::Fax < Order
  validates :FAX番号, :presence => true
 
  has_many :FAX送信結果
 
  def 送信する
    validate!
    FAX送信する
    super
  end
 
  def 送信成功?
    FAX送信結果.送信成功?
  end
 
  private
 
  def FAX送信する
    FAX送信結果.create(送信結果: FAX送信モジュール.送信)
  end
end

メソッドの中から冗長なcase文が消えます。また、relationやvalidationのふるまいも送信方法ごとに整理され、見通しが良くなりました。

このようなクラス設計にしたい場合はSTIを使用します。STIを使用するにはtype:stringカラムを追加します。

  create_table "orders", force: true do |t|
    t.string   "type" # 追加するとSTIになる 
    t.integer  "送信先ユーザ_id"
    t.string   "メールアドレス"
    t.string   "FAX番号"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

typeカラムがある場合とない場合を比較してみましょう。

# typeカラムがない場合 
order = Order::Mail.new(メールアドレス: 'hoge@example.com')
order.save
# id: 1 
# メールアドレス: "hoge@example.com" 
 
# OrderからロードするとOrderになる 
Order.find(order.id)
=> #<Order id: 1, メールアドレス: "hoge@example.com"> 
 
# Order::MailからロードすればOK 
Order::Mail.find(order.id)
=> #<Order::Mail id: 1, メールアドレス: "hoge@example.com"> 
# typeカラムがあり、STIになっている場合 
order = Order::Mail.new(メールアドレス: 'hoge@example.com')
order.save
# type: 'Order::Mail' # クラス名がtypeに自動的にsaveされる 
# id: 1 
# メールアドレス: "hoge@example.com" 
# created_at: 2015-01-01 12-34-56 
# updated_at: 2015-01-01 12-34-56 
 
# Orderからロードしても、きちんとOrder::Mailになる 
Order.find(order.id)
=> #<Order::Mail id: 1, メールアドレス: "hoge@example.com"> 
 
# サブクラスからロードすると、typeを検索条件に自動で加えてくれる 
Order::Mail.all.to_sql
=> "select * from orders where type = 'Order::Mail'"

データベースに保存し永続化した後でもふるまいを維持し続けてくれるので、クラス設計を適切に行えば複雑な仕様が追加されても、良い状態のコードを保てると思います。

運用上の注意

開発には非常に便利なSTIですが、運用を考えて開発しなければならないことが1点あります。typeカラムに存在しないclass名が設定されていると、ActiveRecord::SubclassNotFound例外が発生します。 ActiveRecord::SubclassNotFound例外が発生するのは、2パターンがあります。

  • リリース時に並行稼動している時に、新しいAPで保存した新しいサブクラスを古いAPでロードした
  • リリースしたがロールバックし、新しいAPで保存した新しいサブクラスを古いAPでロードした<

どちらの場合も、何も変更しないサブクラスを作り、先行してリリースしておくと、予期せぬ例外が起きません。フェイルソフトになります。

# 仮置きする。 
class Order::NewClass < Order
end

もしくはデフォルトスコープに現バージョンで存在しているclassのみを指定すると、ロードされないので例外が発生しません。サブクラスを追加した時に追加し忘れたり、where句が思わぬところで効いたりしないか確認が必要です。

class Order < ActiveRecord::Base
  # 条件に加えてしまう。
  default_scope ->{ where(type: [Order, Order::Corec, Order::Mail, Order::Fax]) }
  # ~~ 略 ~~ 
end

おわりに

STI以外にも、ああすればよかったと思うことは多くあります。その中でもSTIについて書いたのは、知っているか知らないかでModelの設計に差が出てしまうため、気づいた時には手軽にはリファクタリングできなくなっているからです。その反面、ある程度の規模のアプリケーションでなければ使い道が無くRuby on Rails チュートリアルに載っていなかったりと軽視されがちです。 新しくmodelを設計するときには頭の片隅に置いておくといいと思います。

RubyKaigi2015レポート

メリークリスマス!

技術開発部のおおはらです。

本日12月25日はクリスマスでもありますが、Ruby2.3のリリース日でもあります!

毎年クリスマスにRubyはバージョンアップリリースを続けており、 開発や利用が盛んであることはRubyエンジニアにとって喜ばしい限りです。

ちなみに私が開発を担当しているコレックはRuby on Railsを使用しています。

さて、先日開催されたRubyKaigi2015に参加してきましたので、そのレポートをお送りしたいと思います。

RubyKaigi2015

1日目

Ruby3 challenges

  • Yukihiro "Matz" Matsumotoさん(rubyの作者)の基調講演
  • Rubyコミュニティのスローガン「MINASWAN」の紹介
  • Ruby2.3.0の新機能の紹介
    • did_you_mean gemがデフォルトでバンドルされる
      • タイポした時に正しいと思われる変数名やメソッド名などを教えてくれる
    • grep_vメソッド
      • パターンにマッチしないものを集める(grep -v)
    • digメソッド
      • 深い階層の要素(例:array[0][1][2])をエラー無く取得
    • indented here document
      • sql = <<~SQLとするとインデントの浅いところに合わせて先頭を削除
    • safe navigation operator(ぼっち演算子) &.
      • &.が膝を抱えて座って、石を寂しそうにながめている人に見えることからぼっち演算子と命名。面白くて覚えやすい!
      • user && user.accountuser&.accountのように簡潔に書ける。 railsのtry!と同じ。
    • Ruby3でStringがimmutableになることへの移行パス(マジックコメント)
  • Ruby3の進捗
    • Ruby2.0と比較して3倍の速度向上を目標
    • そのためにconcurrency(並行性)やJITコンパイラの導入を検討中
  • http://togetter.com/li/911263

Compiling Ruby scripts

  • 笹田耕一さん。Rubyコミッタ。Heroku社でのMatzチームの一員。
  • 性能改善のためにRubyスクリプトの実行前コンパイルを検討中
  • 作成したISeq(InstructionSequence)シリアライザ、デシリアライザの効果
    • 高速起動
    • メモリ削減
    • コンパイル済みコードを別プロセスで共有できる
  • Ruby2.3のsample/iseq_load.rbで試せる
  • 2015_RubyKaigi2015.pdf

Saving people from typos

  • Yuki Nishijimaさん。Pivotal Labs社。kaminari gemのメンテナ
  • Ruby2.3から同梱されるようになったdid_you_mean gemの仕組みの解説
  • spell checkerとmonkey patchで構成
  • spell checker
    • dictionary, control mechanism, optimizationで構成
  • dictionary
    • Symbol.all_symbolsなどで辞書を作成、エラータイプで辞書を分類
  • control mechanism
    • ミスタイプをLevenshtein距離、ミススペルをJaro-Winkler距離+Prefix bonuxで評価
  • monkey patch
    • NameError#to_sをオーバーライドして出力
    • 例外が発生した場所を特定するためにRuby2.3以前はC拡張を使っていたが、Ruby2.3から追加されたNameError#receiverが使えるようになった
  • http://togetter.com/li/911325

Time flies like an arrow; Fruit flies like a banana: Parsers for Great Good

Fast Metaprogramming with Truffle

  • Kevin Menardさん。Oracle社のエンジニア。
  • JRubyとTruffleを使うとメタプログラミングによる実行効率が落ちない
    • 例:method_missing の実行効率が落ちない
  • http://togetter.com/li/911356

High Performance Template Engine: Guide to optimize your Ruby code

  • Kohei Suzukiさん、Takashi Kokubunさん。両名ともクックパッド社。
  • Suzukiさんはfamlテンプレートエンジンを作成
  • benchmark-ips gemでベンチマークし、stackprof, rblineprof gemでプロファイリング、コード改善
  • slimテンプレートエンジンと同じバックエンドのtempleを使用
  • クックパッド社でfamlを本番投入している
  • KokubunさんはHamlitテンプレートエンジンを作成
  • stringを先につなげて高速化
  • いらない機能・オプションを削って高速化
  • Famlより高速だが互換性を一部捨てた
  • Q.同じ会社の同じ部署にいるなら、統合して互換性を失わないモードをオプションで提供しないのはなぜか
    • A. Hamlitは自分の勉強のために作りたかったから。作ったついでにベンチマークで勝ちたかった。
  • https://speakerdeck.com/k0kubun/high-performance-template-engine

TRICK 2015: The second Transcendental Ruby Imbroglio Contest for RubyKaigi


2日目

Keynote

  • KOSAKI Motohiroさん。Rubyコミッタ、Linuxカーネルコミッタ。
  • Linuxデバッグツールの紹介
    • Ftrace
      • Linuxのイベントをトレース
    • Perf tools(パフォーマンスツール群)
      • perf top 実行中のプロセスの関数ごとのtop
      • perf trace 全プロセスが呼んだシステムコールを記録
    • System Tap
      • デバッグスクリプト言語
      • 言語仕様があまり良くない(loopがないなど)
      • CとRubyでシステムコールが取れる
  • http://togetter.com/li/911612

The history of testing framework in Ruby

  • Kouhei Sutouさん。クリアコード社の代表。
  • /¥Atest.+unit¥z/iにマッチするtest unitの違いがわかるようになる話
  • Ruby1.3
    • testsupp
  • Ruby1.4
    • RubyUnit
  • Ruby1.6
    • Lapidary
    • Test::Unit(RubyUnit + Lapidary)
  • Ruby1.8
    • Test::Unit
      • rubyが標準でバンドル。require 'test/unit'することからtest/unitとも表現される
      • メンテナが交代したが、test/unitが複雑でメンテできなくなった
  • Ruby1.9
    • Rubyはtest/unitをバンドルしなくなった
      • test-unit gemとしてリリース(メンテナがSutouさん)
    • minitest
      • 作者はtest/unitのメンテナ)
      • Ruby開発者がminitestのラッパーとして新しくtest/unitを作成してrubyにバンドル
  • Ruby2.1
    • minitest5で互換性のない変更が入った
    • ruby自身のテストにはminitest4を使用
    • ユーザには最新のminitestを提供
  • Ruby2.2
    • minitest5をバンドル
    • 再びtest-unit gemをバンドル(for user)
    • ruby自身のテストにはフォークしたminitest4とtest/unitを使う
  • http://slide.rabbit-shocker.org/authors/kou/rubykaigi-2015/

Turbo Rails with Rust

The worst Ruby codes I've seen in my life

Ruby for one day game programming camp for beginners

  • Ippei Obayashiさん。京大マイコンクラブのメンバー。東北大助教授。
  • 京大マイコンクラブの新入生歓迎イベントとして1日ゲームプログラミングを開催
  • プログラミング初心者である参加者の8割ほどが、動くゲームを完成
  • 成功の原因を考察
    • 参加者と同数のクラブのメンバーがマンツーマンで教えた
    • 1日という締め切りがあること
      • できることの見積がしやすい
      • 型を教えない、グローバル変数で頑張る
      • インストールを簡単にする工夫(ゲームのスターターキット)
  • http://togetter.com/li/911699

Ruby meets Go

Building CLI Apps for Everyone

making robot with mruby

LT

Update Early, Update Often

Automating View Internationalization in Ruby on Rails

  • Rails2から3に変わるときにデフォルトでhメソッド(html_escape)が実行されるのと同じように、I18nのtメソッドやlメソッドがデフォルトで実行されても良いのでは?という提案

A new testing framework Rgot

Building an Unbreakable MRI-based Embedded Computer Appliance

Do you trust that certificate?

How I debugged debugger

Padrino Travel Guide

What I learned by implementing a Ruby VM in Erlang

Rubygemsで作るお手軽データ分析基盤 〜あるいは 私はどうやって他人の褌で相撲を取ったか〜

  • gem選びの基準はシンプル、読みやすい、プルリクの送りやすさ
  • プルリクを送る時に、自分たち固有の機能か、汎用な機能かを切り分けないとマージされにくい

Rationalを最適化してみた

The Mythical Creatures of Summer of Code

  • Rails Girls Summer of Codeの紹介

3日目

Writing web application in Ruby

Refinements - the Worst Feature You Ever Loved

  • Paolo Perrottaさん。Metaprogramming Rubyの著者。
  • Ruby2.1から正式に導入されたRefinementsの解説
  • モンキーパッチは便利だが影響範囲が大きい。その範囲を制限するのがRefinements。
  • あるmodule内で、拡張したいクラスをrefineの引数として定義し、usingキーワードを記述したレキシカルスコープでのみ有効となる
module A
  refine String do
    def shout
      upcase + "!"
    end
  end
end

Class C
  using A
    "hello".shout # => "HELLO!"
  end
end

C.class_eval do
  "hello".shout # => NoMethodError
end

Discussion on Thread between version 1.8.6 and 2.2.3

Plugin-based software design with Ruby and RubyGems

  • Sadayuki Furuhashiさん。トレジャーデータ社の設立者。Fluentdの作者。
  • Fluentd
    • ログの集約ツール
    • RubyGems.orgのgemを使ったプラグインベースのアーキテクチャ
  • Embulk
    • CSVなどのデータ変換・投入ツール
    • RubyGems.orgのgemとJRubyを使ったプラグインベースのアーキテクチャ
  • 課題
    • Ruby VMのバージョン衝突
  • http://www.slideshare.net/frsyuki/pluginbased-software-design-with-ruby-and-rubygems

Request and Response

  • Aaron Pattersonさん。Ruby,Rails,Rackのコミッタ。講演は日本語(すごい)
  • Railsではリクエストをパースするために数十のrack middlewareを呼び出している
  • 開発環境高速化のためにhttp2を使えるライブラリを書いてみた
  • Rack3ではAPIが多分変わる
  • http://togetter.com/li/912368

Actor, Thread and me

  • Masatoshi SEKIさん。Rubyコミッタ。
  • アクターモデルのおさらい
  • 要素としては、アクター、メッセージ、エーテル(メッセージングシステム)
  • アクターは他のアクターにメッセージを送る。自分の都合の良い時に自分宛てのメッセージを受け取る
  • アクターが返事待ちになってシステムが止まることがある
  • https://speakerdeck.com/m_seki/actor-thread-and-me-rubykaigi2015

Keynote


初めてRubyKaigiに参加したのですが、今年はRuby3リリースに向けてどのように高速化していくか、という話題が多かったです。

Rubyそのものの実装の話が多かったので、C言語未経験の自分にとっては少し難解な部分もありました。

Rubyのカンファレンスではありますが、Ruby以外の言語や設計、テスト手法などに触れることができ、幅広く勉強できたと思います。

来年は9月8日~10日に京都国際会館で開催されるようなので今から楽しみです!

記事検索