クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

言われたことをやらないエンジニアになるために

はじめまして。今年の4月にクラウドワークスに入社した新卒エンジニアの戸口(@Dooor)です。自分が初の新卒エンジニアなので、周りのエンジニアが全員中途で入社された方々という贅沢な環境で開発しています。

新卒が1人しかいないのもあって、周りのエンジニアをよく観察してみたりするのですが、言われたこと(だけ*1)をやらないエンジニアが多い印象を受けます。そもそもあれをやれ、これをやれ、と言っている人もいないです。

そんな環境なので「指示は特に出さない」とよく言われます。
配属後すぐに「思っているより放置されると思うよ」とか言われたりします。

昔から言われたことはしっかりできる(と思っている)僕ですが、言われたこと(だけ)をやらないエンジニアになるためにやっていることをお話します。

続きを読む

「アプリだけのオリジナルコンテンツ」をSNSで拡散してもらうために。

こんにちは、クラウドワークスで公式Androidアプリの開発などをやっている @YusukeIwaki です。 クラウドワークスアプリでは、今年の5月に「仕事を探す」機能アップデートをした際に、お仕事特集 というアプリオリジナル機能をリリースしました。

f:id:YusukeIwaki:20160729152317p:plain

お仕事特集機能は、「電車の中で仕事をなんとなく見る」という人のために、クラウドワークス事務局側が温かみのある職人作業(誇張w)で仕事を選んで定期的に掲載しているものなのですが、 今回は、このお仕事特集機能を

といった要望 をどう実現したのか、をお話します。

 

SNSシェアするための要件は?

そもそもTwitterFacebookで拡散させたい、とはどういう機能があれば実現できるでしょう?

答えは簡単で、「URLがあればいい」のです。 逆に、URL以外のメタデータ(画像やdescriptionなど)は、用意しても共有先のアプリで使われないことが多く、「URLがあればいい」が必要十分条件なのです。 (参考→ Y.A.M の 雑記帳: Facebookとそれ以外でACTION_SENDで渡すテキストを変える

では、アプリ独自のコンテンツにURLを紐付けるにはどうすればいいのでしょうか?

ここが今回の記事の本題で、 ディープリンク という仕組みをうまく使います。

ディープリンク

ディープリンクには、大きく分けて2とおりの実装方針があります。

  • アプリが独自スキームを定義して、独自スキームを含むURLをシェアする
  • 特定のhttp(s)のURLへのアクセスをアプリで横取りする

もし仮にほぼ100%のユーザがアプリでリンクを開くことを期待できる場面であれば、前者の方針のように、独自スキーム (androidapp://xxxxx...、crowdworks://xxxx... ) などを定義し、そのリンクをシェアすることで、アプリに誘導をすることができます。 ところが、今回は「SNSでシェアする」という想定のため、PCを使っているユーザも一定数いるのでこの方法は使えません。PCユーザがリンクを踏んだら「androidapp://xxx」や「crowdworks://xxx」というリンクが解釈できずエラーになってしまいます。

f:id:YusukeIwaki:20160729173716p:plain

 

そこで、今回のお仕事特集のシェアでは、後者の方針のように、特定のhttp(s) URLをアプリで横取りする方式を採用しました。

f:id:YusukeIwaki:20160729175019p:plain

「お仕事特集の詳細を見ようとしたらクラウドワークスのアプリが立ち上がってきた!」というモデルにするため、

  • お仕事特集詳細のURLを定義する
  • お仕事特集詳細のURLをPCで見た際の代替動作を定義する
  • アプリ側でお仕事特集詳細のURLを横取りする処理を実装する

の3つの対応をおこないました。

このなかで、2つ目の「お仕事特集詳細のURLをPCで見た際の代替動作を定義する」と3つ目の「アプリ側でお仕事特集詳細のURLを横取りする」という部分について、少し説明を加えます。    

お仕事特集詳細のURLをPCで見た際の代替動作を定義

冒頭で述べた通り、Webサービス本体側にはお仕事特集というコンテンツは存在しません。 だからといって、たとえば「ポケモンGO特集」というリンク文字列をクリックした人がクラウドワークスのトップページに飛ばされたら「あれ??」ってなってしまいますよね。

そこで、今回の実装では、「PCでクリックした時に見せる既存コンテンツ」を管理画面で設定できるようにしました。  

f:id:YusukeIwaki:20160801114438p:plain

これにより、たとえば

  • ポケモンGO特集」→ ポケモンGOの検索結果(新着順)のページ
  • 狙い目ライティング特集 → ライティングカテゴリの仕事一覧ページ

のように、それぞれの特集に対して、違和感のない既存コンテンツを指定でき、PCでリンクを踏んだ際にそれらのコンテンツを見せるようにすることで「あれ??」ってならないような配慮ができます。

 

アプリ側でお仕事特集詳細のURLを横取りする

アプリ側は、単純に https://crowdworks.jp/public/feature_articles/:id を拾うよう、AndroidManifestでインテントフィルタを追加しています。

labelを独自に指定したくなる時が来るかもしれないので、既存のアクティビティに直接インテントフィルタを追加するのではなく、aliasを作成してそこにインテントフィルタを指定すると良いでしょう。

        <activity-alias
            android:name=".PublicFeatureArticleActivity"
            android:targetActivity=".EntrypointActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="https" android:host="@string/hostname" android:pathPrefix="/public/feature_articles" />
            </intent-filter>
        </activity-alias>

エイリアス先のEntrypointActivityでは、URLから特集idを抽出し、当該のお仕事特集にジャンプさせる処理を追加しています。

og:imageを正しく設定するために

おおむね上記の方法でうまくいったのですが、1つだけ問題がありました。

もともと、PCで見た際の代替コンテンツの表示を、単純なHTTP 302リダイレクトで実装していたため、リダイレクト先(仕事検索結果ページなど)のOGPが取得されてしまい、

f:id:YusukeIwaki:20160729182020p:plain

facebookで実際にお仕事特集をシェアしてみると、イマイチな感じになってしまいました。 これでは、「ポケモン特集だ」というのがパット見わからず、バズってもらえません。

そこで、ロボットによるアクセスの場合には、302リダイレクトではなく、HTTP 200でダミーページを返し、そのなかでJavaScriptでリダイレクトをかけるように変更をしました。

修正前
f:id:YusukeIwaki:20160729185208p:plain
修正後
f:id:YusukeIwaki:20160729185852p:plain

ダミーページに指定するog:imageとog:descriptionは、以下のように管理画面で設定できるようにしました。

f:id:YusukeIwaki:20160801180702p:plain

 

 

こうすることで、FacebookからのOGP取得は、お仕事特集の指定画像を参照するようになり、その結果・・・

f:id:YusukeIwaki:20160729202526p:plain

このように、特集をシェアしても違和感がなくなりました。めでたしめでたし。  

 

今後の展開

今回は、「コンテンツをシェアしてもらうために」という思いで、ディープリンクの対応を進めました。

実のところ、対応を進めているときは、マーケティング的にはメリットはあるけど、技術/設計的にメリットは有るのかなー・・・」と複雑化に対する懸念をもちつつやっていました。しかし、対応を進めていくうちに

f:id:YusukeIwaki:20160801135537p:plain

などなど、プッシュとの設計共通化など、新たに設計改善ができそうだなー、というポイントを発見できました。  

クラウドワークスでは、アプリは少人数で開発をしているため、複雑化しちゃったあとにはしっかりリファクタリングし、設計をシンプルに保つ、というのを常に心がけています。 直近は、プッシュ通知とディープリンクの共通設計などを行い、次なるマーケティング施策を打ちやすい土台がためを実施することになるでしょう…。  

 

We're Hiring!

クラウドソーシングのクラウドワークス では、アプリを使って「クラウドワークスをより身近に」するエンジニアを大募集中です!

アプリなら是非やってみたい!APIならガシガシ書けるよ!というかた、お気軽に「話を聞きに行きたい」をポチッとどうぞ!

www.wantedly.com

SQLとの付き合い方

こんにちは、HuluよりNetflix派の田中です(ロゴがお洒落)。

みなさん、SQL書いてますか?

私自身は未だに使う機会が多いことからもなかなか枯れない技術の1つだなと感じながら、日々愛着を持って書かせてもらってます。

ただ、触り始めた頃はわかってないことが多く、プログラミング言語とは勝手が大きく違うことからも、ある程度書けるようになるまでは時間を要した記憶があります。

正直それまでは、あまり好きにもなれませんでした。

今回はそんな経験から、今では好きと言えるレベルになっている「SQLとの付き合い方」をまとめてみました。

クラウドワークス開発におけるSQLとの関わり方

始めに、普段の開発でのSQLとの関わり方を簡単に紹介しておきます。

クラウドワークスの開発ではO/Rマッパー(Ruby on RailsActiveRecord)に任せることが多く、素のSQLはさほど出てきません。

ただ、日常的なデータ分析や抽出作業、または分析基盤の裏側等ではゴリゴリのSQLが必要になることはままある状況です (エンジニア以外の分析チームやマーケティング関係のメンバーもSQLを書きます)。

RDBMSとしてはMySQL、DWHとしてはRedshiftを用いており、ベーシックなSQLを用いつつそれぞれの特性を活かした関数や拡張仕様を利用したりしなかったりしています。

続きを読む

Rails4アップグレードのつまずきどころ

みなさんさようなら、@h3_potetoです。

いよいよRails4アップグレードシリーズもラストです。

最後は、Railsの仕様変更によるアプリの改修です。 これから書くことは、移行前からわかっていて対策していたものもあれば、テストをしたときに発覚したものもあります。 もっとひどいのはリリースした後に発覚したものもあります。 なんて悲しいことなんだ……。

ActiveRecord周り

やはり一番壊れそうなところですから、予想通り壊れましたね……。

絞り込み結果がArrayではなくなる

Rails4では、has_many 関連の中身が ActiveRecord::Associations::CollectionProxy で返ってきます。 かつては has_many 関連も Array として扱えていたため、Array のメソッドを呼んでいたりする場所がいくつかありました。

reject! とかね!

これらのメソッドが undefined method になってしまいます。

配列で欲しい場合は、to_a するとか、target メソッドを呼ぶと配列がもらえるので、それを使って回避できます。

default_scopeの罠

悪名高きRailsdefault_scope ですが、Rails4へのアップグレードでも名前を聞くとは……。

Rails3の場合、joinsやincludesしたときに、default_scope が反映されません。

しかし、Rails4になると、joinsやincludesのときにも default_scope が反映されます。

これ、default_scope に限った話だけではなくて、scope を使っていた他のクエリも、結果が変わるものがいくつもありました。

techlife.cookpad.com

github.com

たとえば、こんなモデルが定義されていたとします。

class Poteto < ActiveRecord::Base
  has_many :poteto_categories
end
class PotetoCategory < ActiveRecord::Base
  default_scope -> { where(remove_flag: false) }
end

このときに、

Poteto.joins(:poteto_categories).where(hoge: "fuga").to_sql

みたいなことをしてみると、結果が変わります。

Rails3

SELECT
  `potetos`.*
FROM `potetos`
INNER JOIN `poteto_categories`
ON `poteto_categories`.`id` = `potetos`.`category_id`
WHERE
`potetos`.`hoge` = 'fuga'

Rails4

SELECT
  `potetos`.*
FROM `potetos`
INNER JOIN `poteto_categories`
ON `poteto_categories`.`id` = `potetos`.`category_id`
AND `poteto_categories`.`removed_flag` = 0
WHERE
`potetos`.`hoge` = 'fuga'

今まで効いていなかった scope が効くようになったってことは、実はRails4になって、むしろ正しく動くようになったってことです。

今までがダメダメだったということが判明して悲しみ……。

serializeのタイミングが変更になっている

あまり褒められたことではありませんが、ActiveRecordのserializeを使って、JSONとかYAMLをDBに保存している場所がありました。 そして、この部分のバリデーションが落ちるようになりました。

Rails3では、serialize(dumpメソッドの呼出)は値の保存時に行われていました。そのため、バリデーションに入ったときには、値はserialize前の値をチェックしていました。

しかし、Rails4.2より、serializeは値を代入したタイミングで行われるようになり、そのため、バリデーション時には既にserialize済みのデータチェックになってしまいます。

Controller周り

paramsのクラスが変更されている

controllerでパラメータを取得したとき、普段は何も意識せずにハッシュとして扱っています。 実はこれ、Rails3では、 ActiveSupport::HashWithIndifferentAccess というクラスでした。

しかし、Rails4になるとこれが、ActionController::Parameters という、ActiveSupport::HashWithIndifferentAccess のサブクラスになります(StrongParameterが入るにあたって追加されています)。

Class: ActionController::Parameters — Documentation for rails (4.1.7)

普通にハッシュとしてアクセスしている限りは何も困ることはないはずです。

ただ、パラメータをDBに保存していたり、パラメータをキャッシュに保存していたりすると話は違ってきます。 特に、今回のように、Rails3とRails4の共存期間がある場合 には、双方のデータに互換性がある必要があります。

この場合で言うと、Rails4になって新しいクラスができているので、Rails4で保存したパラメータのデータはRails3側からは読み込めなくなります。

そもそも、パラメータをそのままDBに保存するとか、そのままキャッシュに保存するとか、かなりよろしくない仕組みであることは間違いないんですが……。

とりあえずこの互換を保つだけなら、ActionController::ParametersActiveSupport::HashWithIndifferentAccess のサブクラスなので、ActiveSupport::HashWithIndifferentAccessに変換してやれば問題なさそうです。 変換したパラメータに関しては、StrongParameter系のメソッドが使えなくなってしまうので、完全にRails4移行して、StrongParameterに書き換えるときまでには、ActionController::Parameters にしておく必要はありますが……。

params[:poteto].to_h.with_indifferent_access

とかしてやれば、変換できます。

いや、やはりrubyのオブジェクトをそのままDBに保存するとか、memcachedに保存しておくとか、非常に良くないと思うんですよね。

DB周り

まさかActiveRecordより深いところで困るとは……。

Rails4になると、DBとの接続がstrict modeに強制されます。

Rails3時代のCrowdWorksは、かなりゆるゆるにsql_modeを設定していたため、strict modeによるエラーが出ました。

もちろん、sql_modeを別のものに指定することもできます。

qiita.com

ただ、本来であるならエラーになるべきであるものを、握りつぶしている事自体がおかしい。 ならばこれを期にstrict modeにしよう。 というわけで、strict modeは維持して、エラーを潰すことにしました。

Data too longって怒られる

varchar(255)のカラムにやたら長い文字列が入っているような場合があります。 一般的にはバリデーションで弾くべきですが、バリデーションがない!

そのため、DBに300文字くらいを入れようとして、エラーになっていました。 場所にもよりますが、ユーザが入力するような場所の場合には、バリデーションを入れましょう。

Incorrect string valueって怒られる

たいていの場合は絵文字で起こっていました。 DBのテーブルがutf8で定義されている場合に、4バイト文字である絵文字を入れようとするとエラーになります。

今までは、悲しいことにエラーを握りつぶして、絵文字以降の文字列を切り捨てて保存していました。

できれば絵文字も保存できるように、utf8mb4にしたいところですが、これはテーブルによってはかなり難しい……。インデックスのサイズが変更になるので、インデックスを貼っていたりすると一筋縄ではいかない。

qiita.com

もちろんutf8mb4になっている方が、今後嬉しいことが多い予感はしますが、それは今回の対応範囲ではなさそうな大きさです。

なので、ここも一応バリデーションで逃げています。

まとめ

一般的な事象ではまったのはこのくらいですが、これ以外にも、モンキーパッチを作っていたりした部分は盛大にコケたりしていました。 たいてい一番悲しいのはここで、特にActiveRecordの実装に大きく依存しているモンキーパッチを作っていると、直すのはかなり辛くなってきます。

解決策を自分たちで考えなきゃいけない上に、そういうものに限ってテストが書きにくかったり、そもそもテストが無かったり(!!)、絶望的です。

今回はそういったモンキーパッチをかなり消しました。 機能的にまだまだ使わない、長大な機能を提供していたり、そもそも現在ではgemで解決できたり、そういったものの大部分は削除する方向になりました。

おかげでRails4アップグレードによって、ソースが少し綺麗になりました。

ただ、それでも消せない機能は残ってしまいます。だから、テストだけは、独自拡張のテストだけは絶対に書いておきたい。

We're Hiring!

期せずして、アップグレードついでに掃除までやりましたが……。 アップグレードを考えると、こういうことをやっちゃいけないなーというものが結構上がったと思います。

クラウドソーシングのクラウドワークス では、こんなものたちを一緒に掃除をしてくれるエンジニアを募集しています。次回Rails5に上げるときはこんな悲しい思いをしなくて済むといいな!!

www.wantedly.com

Rails 4移行時のサービス監視について

⠀人
/ ⁰⊖⁰ \ オカメインコエンジニアの五十嵐(@ganta0087)です。

本連載のなかでRails 3/4の環境を並行稼動させたことについては既にお話させていただきました。 この状態で今までどおりの監視をしていると、何か問題が発生したときにRails 4が原因なのかが切り分けづらくなってしまいます。そのため、次のようなことを追加で行うようにしました。

  • Rails 3と4でエラーアラートの通知を分離
  • Rails 3と4のパフォーマンスのメトリクスを比較

これによってRails 3のときとは異なる事象を発見したり、エラーの原因の調査に役立つことが期待できます。 実際にアプリケーションサーバーでRails 3のときよりも負荷の上がる事象が発生し、特定のコントローラーで問題が発生していることがすぐに特定できました。

今回はこれらの設定方法について紹介します。

f:id:ganta0087:20160714183514p:plain

続きを読む

© 2016 CrowdWorks, Inc., All rights reserved.