食べチョク開発者ブログ

食べチョクエンジニアによるプロダクト開発ブログ

map,filter,reduce関数内で状態を書き換えてはいけないのは、なぜですか

皆さんこんにちは、エンジニアの西尾です。

あなたは今、コードレビューをしています。

以下コードに直面したとき、何を指摘しますか。 修正してほしい点を、どのようにレビュイーに伝えますか。

// これはJavaScriptのコードです。
// 商品の在庫を1つ減らし、売り切れになったものを抽出したい、と思っています。

const soldOutProducts = products.filter(product => {
  product.quantity -= 1;

  return product.quantity <= 0;
});

よくないコードレビューの例

問題は表題の通り、filterの中で状態を書き換えているのが、よくありません。

関数型言語を学んだことがある方なら、このコードの違和感に気がつきます。 filterは純粋関数であるべきだ、副作用を起こしてはいけない。そう認識しているからです。

しかし、それをコードレビューで指摘したとして、相手に伝わるでしょうか。 書いている言語は関数型ではないし、副作用とか言われても意味がわからないし、複雑なコードでもないし、動くからいいんじゃないの。

map,filter,reduce内で状態を書き換えるコードは、思いの外よく見かけます。 そのたびに、私は以下のような指摘をしてしまっていました。

f:id:vividgarden-tech:20210123215718p:plain

f:id:vividgarden-tech:20210123215721p:plain

f:id:vividgarden-tech:20210123215725p:plain

問題は何なのか?

filter内で状態を書き換えてはいけません、と伝えるだけでは、レビューを受けた側も意味がわかりません。 このコードが良くない理由を説明しないと、相手には伝わりません。

なぜ状態を書き換えてはいけないのか。 言われてみると、私自身もすんなり理由を説明できませんでした。

そこで、自分なりにこのコードの問題点を、今一度考えてみました。

複数の目的をもったコードは、わかりづらい

UNIXの基本的な考えに、「ひとつのプログラムには、ひとつのことをうまくやらせる」というものがあります。 目的を最小限に抑えた小さなプログラムは、誰にとってもわかりやすく、保守も容易です。

プログラムに限らず、この考えは1つの関数・1つのブロック・1行のコードにも当てはまります。 複数の目的を持った処理は理解しづらいものです。

今回の例では、「在庫を減らす」操作と「売り切れの商品を抽出する」操作が同時に行われているため、 複雑なコードに見えます。

本来の目的と違う使い方をしているから、わかりづらい

filter関数は、与えられた条件に当てはまるデータを抽出(フィルタリング)するために利用します1。 「フィルターをする」処理をお願いしたのに、在庫を減らす処理も同時にされてしまうのは、驚きがあるロジックです。

mapやreduceも同様で、「渡されたリストを別のものにマッピングする」「渡されたリストを畳み込む」以外の挙動をさせるのは、理解しづらいコードです。

広い範囲に影響を及ぼすロジックは、わかりづらい

呼び出すたびに中身が書き変えられるロジックは、わかりにくいです。

例えば、次のようなメソッド呼び出しで中身も書き換えられてしまったら、直感に反する理解しづらいコードではないでしょうか。

console.log(product); // { name: "商品1", quantity: 3 }

const quantity = getQuantity(product);

console.log(product); // { name: "商品1", quantity: 2 } !? まさか中身が書き換わっているとは
function getQuantity(product) {
  product.quantity -= 1; // 渡された引数の中身を破壊している

  return product.quantity;
}

これは極端な例ですが、今回の例にあるfilterの使い方も、同じようなわかりづらさがあります。

じゃあ、どうすればいいの?

どのように修正すればよいでしょうか。 コード断片だけを見て、適切なアドバイスをするのは難しいです。 在庫を減らすという処理は、別のところで、あらかじめしておくべきでしょうか。設計から見直す必要があるかもしれません。

それでも直すとしたら、以下のようになるでしょうか。

products.forEach(product => {
  // どうしても値を書き換えたかったら、eachを利用する
  product.quantity -= 1;
});

const soldOutProducts = products.filter(product => product.quantity <= 0);

値を書き換える必要があるのなら、forやforEachを使います。 もちろん、書き換えずにすむなら、それに越したことはありません。

おわりに

map,filter,reduce関数内で値を書き換えてしまう違和感を、言語化してみました。 これらの関数をfor(each)と同じ感覚で使ってしまう方が、案外多いように思えます。

今回紹介したメソッドは、あくまで一例です。 例えば、RubyのEnumerableにあるようなメソッドで値を書き換えるのは、避けるべき実装です。

この問題は、コードレビューの時点で、私自身もすんなり理由を回答できませんでした。 あたりまえだと思っていることを見直すことで、自分自身の理解もより一層深めることができました。

www.wantedly.com

www.wantedly.com


  1. 余談ですが、この記事を書くためにリーダブルコードを読み返していました。第3章に、filterという名前は「選択する」のか「除外する」のかあいまいだから避けるべき、との指摘をみつけて、確かに!と納得しました。ただ、JavaScriptにselectはないので、しょうがない。

会社の支給PCがMacBook Pro M1なので、新しく開発環境を構築した話

こんにちは。

今年の年始からジョインした遠藤です。

さて、入社したところ会社支給のMacBook ProがM1チップのものでした。

はい、現状は開発環境で苦労するとか色々噂を聞くやつです。

実際に試したのですが、

  • 現状の開発環境構築スクリプト、手順書が一切使えない
  • VitualBox, Vagrantは利用不可
  • Dockerは利用可能ではあるが、一部イメージが対応されてない
  • 古いパッケージは動かす手段がない

などなど、通常ではぶつからない問題にぶつかります。

食べチョクでは、

  • Ruby
  • Node.js
  • MySQL
  • Redis
  • ElasticSearch
  • Kibana

を利用しています。

この辺りをメインに話つつ、Intel版とこんな風に違うのかっていう辺りの雰囲気を感じ取っていただければと思います。

どこに開発環境を構築するか

まず、どこで開発環境を構築するかを考えてみたいと思います。

  • ローカルで開発環境を作る
  • 仮想化ソフトウェア上で作る
    • VirtualBox🙅‍♂️
    • Docker
  • サーバー立ててその中に開発環境を作成して、リモートで作業する

この3種類になると思いました。

支給されたPCだとローカルに開発環境を構築される方が多いのかなって印象が多いのですが、どうなのでしょうか。

私は支給されたPCだとローカルに開発環境を構築したい派なので、ローカルでの開発環境構築をひとまず目標にしました。

今回は「ローカルで開発環境を作る」「仮想化ソフトウェア上で作る」で試しました。

では、開発環境の構築をしていきます。

ローカルで開発環境を作る

ここから実際に行ったことを記述していきたいと思います。

homebrew

みんな大好きのhomebrewですが、きちんと公式のドキュメントを読みましょう。

インストールする場所がm1 macの場合は別になっています。

mkdir /opt/homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
echo "export PATH=/opt/homebrew/bin:$PATH" >> ~/.zshrc
source .zshrc

However do yourself a favour and install to /usr/local on macOS Intel, /opt/homebrew on macOS ARM, and /home/linuxbrew/.linuxbrew on Linux. Some things may not build when installed elsewhere. One of the reasons Homebrew just works relative to the competition is because we recommend installing here. Pick another prefix at your peril!

Installation — Homebrew Documentation

Intel版ならディレクトリを明示するとかなかったので、いつもと違うなっていうことを、最初から感じさせてもらえます。

rbenv

Rubyのバージョン管理ツールのrbenvです。

こちらがないとRubyのバージョン管理で困るので、導入しましょう。

GitHub - rbenv/rbenv: Groom your app’s Ruby environment

brew install rbenv
rbenv init
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash

ここまでは問題なくいきます。

さて、待望のインストールをします!?

rbenv install 2.5.7
Downloading openssl-1.1.1i.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/e8be6a35fe41d10603c3cc635e93289ed00bf34b79671a3a4de64fcee00d5242
Installing openssl-1.1.1i...
Installed openssl-1.1.1i to /Users/xxx/.rbenv/versions/2.5.7

Downloading ruby-2.5.7.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.7.tar.bz2
Installing ruby-2.5.7...

WARNING: ruby-2.5.7 is nearing its end of life.
It only receives critical security updates, no bug fixes.

ruby-build: using readline from homebrew

BUILD FAILED (macOS 11.0.1 using ruby-build 20201225)

Inspect or clean up the working tree at /var/folders/np/m8mjm0q52njgyqc6kp0b_zw80000gn/T/ruby-build.20210110220451.39283.BzZl54
Results logged to /var/folders/np/m8mjm0q52njgyqc6kp0b_zw80000gn/T/ruby-build.20210110220451.39283.log

Last 10 log lines:
compiling ../.././ext/psych/yaml/reader.c
3 warnings generated.
compiling ../.././ext/psych/yaml/emitter.c
compiling ../.././ext/psych/yaml/parser.c
linking shared-object date_core.bundle
5 warnings generated.
linking shared-object zlib.bundle
1 warning generated.
linking shared-object psych.bundle
make: *** [build-ext] Error 2

ログファイルを確認します。

compiling qsort.c
linking shared-object -test-/vm/at_exit.bundle
linking shared-object -test-/wait_for_single_fd.bundle
compiling closure.c
compiling nofree.c
compiling conversions.c
compiling zlib.c
installing default libraries
compiling fiddle.c
compiling psych_to_ruby.c
closure.c:264:14: error: implicit declaration of function 'ffi_prep_closure' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
    result = ffi_prep_closure(pcl, cif, callback, (void *)self);
             ^
1 error generated.
make[2]: *** [closure.o] Error 1
make[2]: *** Waiting for unfinished jobs....

なんか今までにあまり起こったことのないエラーに遭遇します。

ffiでエラーが起こっているのですが、libffiを除外すればインストールできます。

Building Ruby on arm64 macOS

この問題が2020/6/30に解決されていることに感動を受けつつ、除外しましょう。

RUBY_CFLAGS=-DUSE_FFI_CLOSURE_ALLOC rbenv install 2.5.7

コマンド自体はissueを参考にさせていただきました。(先人に感謝)

2.6.6 on ARM64 · Issue #1699 · rbenv/ruby-build · GitHub

Ruby3.0の場合はなんの問題もなくインストールできます。

MySQL

brew install mysql@5.7
echo 'export PATH="/opt/homebrew/opt/mysql@5.7/bin:$PATH"' >> ~/.zshrc
brew services start mysql@5.7

正直ここは何も問題がないので、休憩ゾーンになります。

Redis

brew install redis
brew services start redis

これも特に問題はありません。

Node.js

Node.jsは今のところversion 15以上じゃないと対応されてない状態なので、ここは素直にversion 15を使いました。

webpackのコンパイルでしか利用しないので、まぁ、いいかという気持ちもあり。。。

Nodejs 14.x doesn't compile on ARM OSX (M1) · Issue #36161 · nodejs/node · GitHub

弊社ではnodebrewを利用していたので、下記を参考にさせていただきました。

brew install nodebrew
echo "export PATH=$HOME/.nodebrew/current/bin:$PATH" >> ~/.zshrc
vim $(which nodebrew)

sub system_info {
    my $arch;
    my ($sysname, $machine) = (POSIX::uname)[0, 4];

    if  ($machine =~ m/x86_64|arm64/) {
        $arch = 'arm64';
    } elsif ($machine =~ m/i\d86/) {

...

nodebrew compile v15.5.0

M1 Mac を購入して arm64 縛りでインストールしたもの (更新中) - アルパカ三銃士

ElasticSearchのインストール

弊社では検索エンジンにElasticSearchを利用しているので、ElasticSearchをインストールします。

brew install --build-from-source elasticsearch
...

==> Installing dependencies for elasticsearch: openjdk and gradle
==> Installing elasticsearch dependency: openjdk
==> Patching
==> Applying f80a6066e45c3d53a61715abfe71abc3b2e162a1.patch
patching file src/hotspot/share/runtime/sharedRuntime.cpp
Hunk #1 succeeded at 2850 (offset -6 lines).
==> Applying 4622a18a72c30c4fc72c166bee7de42903e1d036.patch
patching file src/java.desktop/macosx/native/libawt_lwawt/awt/CSystemColors.m
==> ./configure --without-version-pre --without-version-opt --with-version-build=9 --with-toolchain-path=/usr/bin --with-sysroot=/Library/Developer/Com
Last 15 lines from /Users/endo/Library/Logs/Homebrew/openjdk/01.configure:
checking for stdlib.h... yes
checking for string.h... yes
checking for memory.h... yes
checking for strings.h... yes
checking for inttypes.h... yes
checking for stdint.h... yes
checking for unistd.h... yes
checking stdio.h usability... yes
checking stdio.h presence... yes
checking for stdio.h... yes
checking size of int *... 8
configure: The tested number of bits in the target (64) differs from the number of bits expected to be found in the target (32)
configure: error: Cannot continue.
/private/tmp/openjdk-20210110-26180-4xtn4y/jdk15u-jdk-15.0.1-ga/build/.configure-support/generated-configure.sh: line 82: 5: Bad file descriptor
configure exiting with result code 1

Do not report this issue to Homebrew/brew or Homebrew/core!

These open issues may also help:
Cassandra 3.11.9_1 crashes in openjdk@8 JVM https://github.com/Homebrew/homebrew-core/issues/66462
openjdk: Add support for Apple silicon https://github.com/Homebrew/homebrew-core/pull/65670
OpenJDK is somewhat broken on newer MacOS instances, console is flooded with errors when using JMeter, AdoptOpenJDK has no issues https://github.com/Homebrew/homebrew-core/issues/66953

案の定エラーになります。

原因はOpenJDKがサポートされてないので、利用できません。

https://github.com/Homebrew/homebrew-core/pull/65670

このPR早くマージしてくれ!という気持ちもありつつ、OpenJDKはZuluがM1対応しているので、そちらを利用します。

Support of Java Builds of OpenJDK for Apple Silicon | Azul

Java Download | Java 8, Java 11, Java 13 - Linux, Windows & macOS

※記事執筆中にマージされていましたので、この問題は解決していましたが、https://github.com/Homebrew/homebrew-core/pull/69029で別の問題が発生しています

ElasticSearchはソースコードからビルドするように変更します。

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.1-darwin-x86_64.tar.gz
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.1-darwin-x86_64.tar.gz.sha512
shasum -a 512 -c elasticsearch-7.10.1-darwin-x86_64.tar.gz.sha512 
tar -xzf elasticsearch-7.10.1-darwin-x86_64.tar.gz
cd elasticsearch-7.10.1/ 

Javaの参照先をZuluに追加します。

Set up Elasticsearch | Elasticsearch Reference [7.10] | Elastic

echo "export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-15.jdk/Contents/Home" >> ~/.zshrc

これでbin/elastic searchで動くのですが、xpackがサポートされないので、コンフィグから外しましょう。

org.elasticsearch.ElasticsearchException: X-Pack is not supported and Machine Learning is not available for [darwin-aarch64]; you can use the other X-Pack features (unsupported) by setting xpack.ml.enabled: false in elasticsearch.yml

Kibana

ElasticSearchを入れるので、Kibanaも入れましょう。

homebrew経由でインストールしようとすると、案の定エラーになるのでソースコードからビルドします。

curl -O https://artifacts.elastic.co/downloads/kibana/kibana-7.10.1-darwin-x86_64.tar.gz
curl https://artifacts.elastic.co/downloads/kibana/kibana-7.10.1-darwin-x86_64.tar.gz.sha512 | shasum -a 512 -c - 
tar -xzf kibana-7.10.1-darwin-x86_64.tar.gz
cd kibana-7.10.1-darwin-x86_64/

bin/kibana

ここまで揃うと、なんとかbundle exec rails serverまでは辿りつけると思います。

ここまでがローカル開発環境編です。

仮想化ソフトウェア上で作る

現状はVirtual Boxが対応していないので、Docker Previewが頼みの綱になります。

Docker Previewはrosettaが現状は必要になるので、rosettaをインストールします。

softwareupdate --install-rosetta

Docker Previewのダウンロードは下記にリンクがあるので、そちらかダウンロードします。

Download and Try the Tech Preview of Docker Desktop for M1 - Docker Blog

ここではうまくいきづらいものだけ紹介します。

docker-composeを利用することを前提にしていきます。

MySQL

設定ファイルです。

services:

  mysql:
    image: mysql:5.7
docker-compose up
Pulling mysql (mysql:5.7)...
5.7: Pulling from library/mysql
ERROR: no matching manifest for linux/arm64/v8 in the manifest list entries

ということで、イメージとして対応していないです。

no matching manifest for linux/arm64/v8 in the manifest list entries · Issue #5142 · docker/for-mac · GitHub

が、issueを見るとMariaDBを使うか、特定のIntelのやつを使えばいけるということで、変更します。

services:

  mysql:
    image: mysql:5.7@sha256:b3b2703de646600b008cbb2de36b70b21e51e7e93a7fca450d2b08151658b2dd

Kibana

arm64のイメージがないので、利用できませんでした。

docker-compose up
WARNING: Found orphan containers (tabechoku_elasticsearch, tabechoku_redis, tabechoku_mysql) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up.
Pulling kibana (kibana:7.10.1)...
7.10.1: Pulling from library/kibana
ERROR: no matching manifest for linux/arm64/v8 in the manifest list entries

Docker Hub

ElasticSearch/Redisに関しては問題なかったので、割愛させていただきます。

結論

正直なんとか開発状態までは漕ぎ着けれるのですが、どっちかというと現場で作業を行う場合に最新のバージョンになっていないことが多いので、そこをどうするかというのが、ポイントになるかと思います。

明らかに対応外なら、割り切って最新のを使うようにするしかないです。

Dockerの場合は、利用できない可能性もあるのでそこも割り切るしかないのかなと思います。

この辺りで本番環境と乖離が起こる可能性が高いので、どこまでを許容とするか、あるいは本番環境を合わせるかになるのかなと思います。

また、今回は先人の知恵があり、なんとかなりました。

  • 解決方法を書いてくれている方
  • M1に対応しようとOSSで対応している方

など、実際に対応されている方には、感謝しかありません。

よく言われていることですが、開発環境、ライブラリは常日頃から最新化しておかないと苦労する を痛感します。

こういった問題に解決するためにも今はチームとして頑張っています。

※この記事を書いている間にもRuby 2.6.6にアップデートし、今はRuby 2.7系にアップデートに取り組んでいます。

弊社ではM1 Macbook Proで開発をするエンジニアを募集しております。

www.wantedly.com

Railsバリデーションメッセージのリソース名を特定するパッチの話

こんにちは。食べチョクの開発をお手伝いしているフリーランスエンジニアのもぎゃ です。
ぼくも記事書いていいそうなので、最近食べチョクの開発をやっている時に発見したRailsテクニックの話を書きます。

バリデーションメッセージのつらみ

Railsでバリデーションをつかって入力をチェックしている場合、@model.errors.full_message をエラーメッセージとして出すことができます。

Active Record バリデーション - Railsガイド

ただ、ぼくらが作ったモデルの日本語名までRailsが知っていてくれるわけではないので、"User Messageを入力してください"みたいな分かりづらい文字列になってしまうことがあって、この場合i18nのリソースを適切に書いてあげることでエラーメッセージを改善することが出来ます。

Rails 国際化 (i18n) API - Railsガイド

ということで、エラーメッセージのカスタマイズについては公式ドキュメントに書いていただいているのですが、現場のRailsのモデルは"User"みたいにシンプルなものではなくて、"Users/address"みたいにnamespaceがついていたり、modelではなくformオブジェクトのバリデーションだったり、accepts_nested_attributes_forを使っていたりすると、「え、これどこに名前を書けばいいの?」となることもしばしば。

この場合、エラーメッセージは「リソースが適切な位置にあれば使われる」というヒントしかないので、うまく行かない場合、「これか?」「ここなのか?」「これもちがうの?」という"当て物ゲーム"を繰り返すしかないのが辛いところです。
「もう良いからif文書いて自前のエラーメッセージにしようぜ」みたいなごまかした実装をするたびに、ControllerがFat化していく...

そもそも、「リソースが適切な位置にあれば使われる」というルールである以上、i18nのライブラリは"正解"のリソースを読みに行っているはずです。そのリソース名を教えてくれたら一発じゃない!?

リソース名を教えてくれるモンキーパッチ

細かい実装はさておき、最終的にはi18n.tを呼び出しているはずです。i18n.tのエイリアス元であるtranslateメソッドの実装はこちら。

  def translate(key = nil, *, throw: false, raise: false, locale: nil, **options) 

i18n/i18n.rb at master · ruby-i18n/i18n · GitHub

この最初のkeyが目的のリソース名です。じゃあ最初からこれを教えてよ。ということでパッチを書きました。

ファイル名は何でも良いので、config/initializerの下におきます。

  module I18nMonkeyPatch
    def translate(key = nil, *, throw: false, raise: false, locale: nil, **options)
      Rails.logger.warn "***************************************************"
      Rails.logger.warn "i18nMonkeyPatch key: #{key}"
      Rails.logger.warn "***************************************************"
      super
    end
  end

  I18n.singleton_class.prepend(I18nMonkeyPatch)

そうすると、i18nの変換が行われるたびにログにリソース名が出力されるので、該当箇所を実行してみればこんな感じでリソース名が分かるようになります

 ***************************************************
 i18nMonkeyPatch key: activerecord.attributes.order_inquiry_thread/lines.message
 ***************************************************

ということで、こういうリソースファイルを作ればよいようです

  ja:
    activerecord:
      attributes:
        'order_inquiry_thread/lines':
          message: "メッセージ"

答えを知ってしまえば、"i18n モデル スラッシュ"とかでググってそれらしい記述を見つけ出せるのですが、そもそもなぜ駄目なのかがわからないと検索キーワードすらわからないので、一発で答えを得られるパッチはそれなりに使い所があるのではないかなと思います。

最後に

食べチョクでは、開発者の方を募集しております。
ぼくみたいにフリーランスの立場から開発に参加してissueをガシガシ消化していけるエンジニアも必要だし、急成長する会社の屋台骨として開発を推進できる社員さんはもっともっと必要そうです。

この記事を見て、「おおそんなやり方が」と思った方、ぜひ一緒にやりましょう!
「その程度かよ」と思った人、野菜の新しい流通経路を作るためにぜひその腕前を貸してください!

www.wantedly.com
www.wantedly.com

データベース設計の際に気をつけていること

皆さんこんにちは、エンジニアの西尾です。

新しい機能・サービスを開発する際、私は特にデータベース設計に気をつかいます。

データベースはシステムの土台です。 土台が不安定だと、その上に積み上げていくアプリケーションコードがいびつなものになり、つらい思いをします。 また、一度動き出してしまったシステムのデータベース設計を変えるのは、容易なことではありません。

データベース設計には”これだ!”という正解はないと思っています。 サービスの特徴、システムの性質、toB向け/toC向け、Readが多い・少ない、Writeが多い・少ない。 その他もろもろの背景により、データベース設計の仕方も変わってきます。

このテーブルは正規化していないから駄目だ、この設計はいわゆるポリモーフィック関連だから使ってはいけない、などということはありません。 アンチパターンと呼ばれるものも時と場合によっては正解になります。

今回は、食べチョクのデータベースを設計する際に気をつけていることを共有いたします。 なお、食べチョクではRDBとしてMySQL5.7を利用しています。

1. 制約をつける

データベース設計において重要なのは、いかにして不整合を起こさないようにするかです。

「データを引いてみたら関連先のレコードが無くなっている」、「このレコードはユーザーごとに1つだけ持つはずだけど、2レコードある」など。 不整合は往々にして発生します。

データを挿入・更新・削除してもよいかのチェックはアプリケーションレベルで防ぐだけではなく、可能ならばデータベースレベルで行います。 そのために、以下制約をつける努力をします。

1.1 外部キー制約をつける

外部キー制約は、可能な限りつけるようにしています。 DBが別れている場合、外部キーはもちろん貼れないのですが、そうでない場合はとにかく何も考えず貼っています。

テスト時のテストデータが入れにくいから貼りたくない、とかいってる場合じゃないです。本番環境で不整合が起こる方が怖いですよね。 テストデータ入れるだけなら、 SET FOREIGN_KEY_CHECKS=0; とかでレコードいれればよいだけですし(本番環境ではやらないでください)。

1.2 ユニークキー制約をつける

ユニークキーも可能な限りつけるようにしています。

例えば以下のような注文(orders)テーブルと、支払い(payments)テーブルがあるとして、 注文に対する支払いは1つしか存在しないことがわかっている場合。

CREATE TABLE `orders` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  CONSTRAINT `orders_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `payments` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_id` int(11) NOT NULL,
  `transaction_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  CONSTRAINT `payments_order_id_fk` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`)
) ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

このときは、paymentsテーブルのorder_idにユニークキー制約をつけます。

CREATE UNIQUE INDEX `index_payments_on_order_id` ON `payments`(`order_id`);

そうすれば、注文に対する有効な支払いがなぜか2件できてしまっていた、というバグを未然に防ぐことができます。

別のパターンも見てみます。 例えば、paymentsテーブルに以下のようにactiveというカラムを足します。 有効な支払いは1つ(active=1の場合)だけだが、支払いを変更したなどで履歴情報として過去の支払いをactive=0として持っておきたい場合。

CREATE TABLE `payments` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `active` tinyint(1) NOT NULL DEFAULT '1',
  `order_id` int(11) NOT NULL,
  `transaction_code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `payments_order_id_fk` (`order_id`),
  CONSTRAINT `payments_order_id_fk` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;

この場合、(active, order_id)にユニークキー制約を貼るわけにはいきません。 支払いを2回変更したら、active=0が2レコードできてしまうのですが、そういうレコードは入れられません。

この場合は以下2つのうち、どちらかで修正します。

そもそもactiveとかいうカラムを持たず、別途payment_historiesなどのテーブルを作り、そちらにレコードを移す

履歴として取っておく必要があるのなら、元のテーブルにレコードを残しておくのではなく、別のテーブルに移動させます。 こちらの設計の方がスマートです。元のユニークキー制約を残したまま、レコードの行数も抑えられるし、paymentsを引くときの余計な条件式も減らせます。

activeをNULL許容にして、過去データは active=NULL としてデータを入れる

NULLは必ずユニーク扱いになります。そのため、activeカラムをnull許可することで、(active, order_id)にユニークキーを貼っていても、 (MySQLでは)以下のようにデータを入れることが可能です。

INSERT INTO payments(active, order_id, transaction_code, created_at) VALUES (1, 1, "AAA1", "2020-06-15 00:00:00");
INSERT INTO payments(active, order_id, transaction_code, created_at) VALUES (NULL, 1, "AAA2", "2020-06-15 00:00:00");
INSERT INTO payments(active, order_id, transaction_code, created_at) VALUES (NULL, 1, "AAA3", "2020-06-15 00:00:00");

この設計は賛否両論あると思いますが、これはNULLの正しい使い方ではないかと思っています。

別テーブルに分けるか、NULL許容して入れるか、実装コストを踏まえて選択しています。

1.3 NOT NULL制約をつける

なるべくNULLが入らないようにテーブルを設計しています。 例えば以下のようなテーブルがあったとします。

CREATE TABLE `something` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `note` varchar(255),
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

このテーブルからnoteが空のレコードを抽出する場合、どういうSQLを書くでしょうか。

SELECT * FROM something WHERE note = "";

いやいや、もしかしたらnoteにはNULLが入っているかもしれません。

SELECT * FROM something WHERE note IS NULL OR note = "";

NULLが入っていることで、SQLが複雑化する可能性があります。 また、SQLの書き方を間違えると意図したレコードを抽出できない可能性があります。 そのため、極力NULLを入れなくて良い箇所はNOT NULL制約を入れるようにしています。

ただし、なんでもかんでもNULLを排除すれば良いというわけではありません。 例えば、以下のようにuser_idを持つテーブルがあるとします。

CREATE TABLE `something` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11),
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

なんとしてもNULLを入れたくないのか、user_idデータが無い場合はuser_id=0としてレコードを作成する、 といったように無理やりダミーレコードをいれてNULL回避をする方がいます。 データが存在しないのならば、素直にNULLを入れるべきです。

余談ですが、datetime型に0000-00-00 00:00:00を入れるとえらい目にあうので注意が必要です。 ( 参照: https://soudai.hatenablog.com/entry/2018/05/12/191050 )

2. 適切なデータ型を使う

MySQL5.7では、https://dev.mysql.com/doc/refman/5.7/en/data-types.html にあるデータ型を利用できます。

本来数値しか入らないレコードはint型を使うなど、適切な型を設定するよう努力します。 数値カラムに全角数字が入っていた、そんなことはデータ型を正しく選択していれば起こり得ないことです。 なんでもかんでもvarcharで定義しないよう注意します。

以下、型を選ぶ時に気をつけていることを記載します。

  • bool値を入れるカラムには tinyint(1)を利用する
  • 長くない可変文字列を入れる場合(大体1024文字より下くらいかな?)はvarcharを利用します
  • 長い可変文字列を入れる場合はtext型を利用する、またtext型を使う場合はテーブルのROW_FORMATがDynamic(Barracuda)であることを確認する
    • MySQL5.6まではデフォルトのフォーマットはCompact(Antelope)でしたが、これは1レコードあたり8KBまでしかデータを入れることができません。
    • テキスト型を使うと8KB制限を突破してしまうこともあるため、テーブルのフォーマットがDynamic(Barracuda)であることを確認します。
  • 整数値を入れる場合はint型かbigint型を使う
  • float型は使わない
    • 精度のトラブルに巻き込まれたくないためfloatは使いません。多くの場合、doubleかdecimalで問題ありません。
  • 金額情報など、精度を求められる小数値にはdecimalを使う
    • doubleも小数点以下の精度に悩まされることがあります。金額を扱う、精度が必要な計算は必ずDecimalを利用します。
  • 日付を入れる場合はDATE型を使う
    • 商品のお届け日など、日付を入れる場合は DatetimeやTimestamp型ではなくDATE型を使うようにします。DatetimeやTimestampはタイムゾーンの影響を受けるためです。
  • JSON型を使ったら負け

注意点として、数字に見えるデータを入れる際、必ずしもint型が正解ではない可能性もあります。 郵便番号(例: 0100011) とか、電話番号(例: 08000000000) は一見数値に見えますが、これはvarcharで保存すべきです。 なぜなら、数値の場合は先頭の0の値が抜け落ちてしまうためです。

3. 三角関係のリレーションを持つテーブルは(できるだけ)作らない

三角関係という表現が正しいかはさておき、下図ERのようなテーブルは(できるだけ)作らないようにしています。

f:id:vividgarden-tech:20200614205008p:plain

この構造では、注文からグループを引く場合に注文 → ユーザー → グループというたどり方をした場合と、注文 → グループと 直接レコードを引いた際に違うレコードが取れる可能性はあります。 例えば、ユーザーが所属するグループが途中で変わった場合は、整合性が崩れる可能性があります。

三角関係にならないよう、以下のように設計を修正します。

f:id:vividgarden-tech:20200614205031p:plain

性能問題により、注文からグループテーブルへのショートカットを貼る場合もあります。 その場合、ユーザーが所属するグループが変わった場合は、注文レコードが保持しているグループへのリレーションも貼り直す必要があります。 整合性が崩れやすいので、このような設計はできるだけしないようにしています。

4. 一時的なレコードと永続化が必要なレコードを同じテーブルには入れない

一時的に利用するレコードと、永続化すべきレコードは同じテーブルに入れないようにしています。

例えば、以下のような受注(orders)テーブルがあるとします。 これは SpreeというオープンソースのECプラットフォームのテーブル設計です。 (実際はもう少し複雑なのですが、簡略化しています)

f:id:vividgarden-tech:20200614205054p:plain

Spreeにはカート機能があるのですが、ユーザーが一時的にカートに入れた注文も、その後実際に受注したデータも同じordersテーブルに入っています。 カートに入っている未発注の注文は、注文ステータスをcartという状態にして、関連テーブルと共にレコードを挿入してます。

確かにカートの情報をそのままordersに入れれば実装上は楽なのですが、この設計は性能に問題がでる可能性があります。 ユーザーがカートに入れたまま放置したら、ordersテーブルには不要なレコードがたまり続けることになります。

構造が同じでも意味が違うデータは分けて管理すべきだと、私は思います。 その方がレコードの行数も少なく抑えられて、注文テーブルを引くコストが下がります。

5. 定期的に性能を見て設計を検討し直す

データベースは生き物です。

日々データ量が増え続けていき、設計時には思いもよらないようなパフォーマンス問題にぶち当たることがあります。 一度設計したら終わりではなく、日々パフォーマンスを監視し、重いクエリがあればチューニングが必要です。 ときにはテーブル構造自体を見直す必要があるかもしれません。

昔はSlowQueryを吐き出してそれを見る、ということをしていましたが、 最近では性能を監視するためのツールが充実しています。 おすすめのツールはNewRelicとAWSのRDSパフォーマンスインサイトです。

NewRelicでは、下図のようにパフォーマンスを可視化してくれます。 例えば、以下を見るとDeliveryPossibilityテーブルを引くのに時間がかかっていますね。パフォーマンスチューニングが必要そうです。

f:id:vividgarden-tech:20200614205115p:plain

AWSのパフォーマンスインサイトは、下図のように更に細かい単位でパフォーマンスの可視化をしてくれます。

f:id:vividgarden-tech:20200614205126p:plain

f:id:vividgarden-tech:20200614205138p:plain

6. インデックスの性質を知り、適切に貼る

パフォーマンスが出ないクエリは、インデックスを貼れば早くなる!わけではありません。 インデックスは銀の弾丸ではなく、性質を知り適切に貼る必要があります。

6.1 カーディナリティを考慮してインデックスを貼る

MySQLでインデックスを利用する場合、多くはBツリーインデックスを利用するかと思います。 Bツリーだろうがハッシュだろうが、カーディナリティの低いカラムにインデックスを張っても性能はでません。

カーディナリティは、入っている値の種類がどのくらいあるかを表しています。

例えば、レコードが1千万行あるテーブルがあり、activeという1と0の値を取るカラムがあるとします。 このactiveカラムのカーディナリティは2であり、ここにindexを貼っても多くの場合クエリは早くなりません。 逆にカーディナリティが1千万のカラム(すなわち重複の無いデータ)にindexを貼れば、早くデータを引いてこられる可能性があります。

カーディナリティを確認するには、show indexクエリを利用します。 以下のテーブルでは複合indexとして(faxable_type, faxable_id)にインデックスを張ってます。 faxable_typeはカーディナリティ2であり、(ユニークキー制約がある場合は別として)ここに貼る意味はあまりなさそうですね。 faxable_idはカーディナリティが高く、このカラムにインデックスを貼るのは正解です。

f:id:vividgarden-tech:20200614205159p:plain

6.2 インデックスが効いているかをExplainで見る

インデックスが効いているかはExplainを使えばわかります。 Explainの解説をするとそれだけで長文がかけてしまうのでここでは省略します。 クエリにexplainをかけてtypeを見る、possible_keysに意図したインデックスが入っているか、引いてくるレコードの行(rows)が少なくなっているか(フルスキャンしていないか)、Using Indexがでているか、コストが高くないかなどを確認します。

MySQL Workbenchを使うと、グラフィカルにExplain結果が見られるのでおすすめです。

f:id:vividgarden-tech:20200614205211p:plain

6.3 インデックスを貼る順序に気をつける

複合インデックスの場合は、インデックスを貼るカラムの順序も重要です。

(column1, column2, column3) という順に貼ったindexと、(column3, column2, column1) のように貼ったインデックスは別のものとして扱われます。 前者のindexを貼った場合、条件に(column1), (column1, column2), (column1, column2, column3)が入っている場合のみ、インデックスが有効です。 where column3=xxxxx のようにクエリを書いてもindexは使われないことに注意します。

6.4 インデックスショットガンをしない

インデックスを全部のカラムに貼ったら早くなるんじゃないか。 誰しもが思う疑問かもしれませんが、そんなに都合良くは行きません。

まずMySQLのインデックスは1クエリにつき基本的に1つしか利用できません。 (インデックスが1つだけ使われるということで、1つのカラムにだけ効く、という意味ではありません。複合インデックスは有効です。)

index1, index2をいい感じに合体して高速なクエリを作り出してくれる、そんな高度な機能は... インデックスマージという機能はなくはないのですが、基本は1つだけ使うと考えて良いでしょう。

またインデックスを貼るとテーブルの更新処理が遅くなり、ディスク容量も膨れ上がります。 それはそのとおりで、通常のレコードとは別にインデックス用のデータを作るわけですから、遅くなります。

すべてのカラムにインデックスを貼る、インデックスショットガンはしないようにします。インデックスを貼るにもコストがかかるのです。

7. 正規化が必要なところ、不要なところを見極めてテーブル設計をする

MySQLのjoinはとても遅いです。Oracleなら高速で返ってくるであろうクエリも、MySQLでは遅いです。

toC向けのRead負荷が高いページを表示するのに、joinを多用したクエリを実行するべきではありません。 アクセス負荷の高いページではjoinしたら負けです。理想は1テーブルのみのアクセスです。

そういう場合は、各種テーブルのデータをサマった正規化していないテーブルを別途作成し、そのテーブルにのみアクセスするようにします。 いわゆるマテビュー(マテリアライズド・ビュー)のようなものを別途作成することで、アクセスを高速にさばくことができます。

逆にtoB向けのページでは、できるだけ正規化したテーブルにアクセスするようにします。 こちらは整合性の崩れた(キャッシュのような)レコードにアクセスしてデータが間違っていた、という事件が起きるほうがリスクが高いです。

正規化する/しないはアクセスの性質・サイトの性質に合わせて柔軟に検討します。

8. リレーショナルデータベース(RDB)が苦手な表現を理解し設計する

RDBは万能ではありません。RDBでは表現が難しいデータ構造・表現も存在します。 以下はRDBで扱いにくいものの一例です。

  • ツリー構造など階層を持ったデータ構造
  • カラムが動的に変化するデータ構造
  • GIS(地理情報)を扱う場合
  • 全文検索が必要な場合
  • データ量が多すぎる場合(アクセスログデータを入れて計算するなど)

ツリー構造(階層構造)を持ったデータは頻出ですが、RDBでは扱いにくいものの部類にはいります。 そのため、ツリーを表現するための設計パターンが存在します。 隣接リスト・Nested Set・クロージャーテーブルなどなど。どれも一長一短があります。それぞれの設計の利点欠点を理解し、適切なデータ構造を選びます。

カラムが動的に変化する構造も苦手です。GISも苦手、全文検索も苦手です。データ量が多すぎる場合もRDBでは対応できないかもしれません。

RDBに向いていないデータを入れる場合は、素直にRDB以外を選択したほうが良いでしょう。

まとめ

今回はデータベース設計を行う際に気をつけていることをまとめました。

システムの土台となるデータベースの設計は、とても重要です。 DBの寿命はアプリより長い といわれていますが、そのとおりだと思います。

あとでつらい思いをしないためにも、今後も慎重にテーブル設計をしていきたいものです。

www.wantedly.com

www.wantedly.com

コンビニ決済が利用できるようになりました! 2020年1月のカイゼン内容のご紹介

こんにちは、エンジニアの鹿倉です。

2020年もどうぞ食べチョクをよろしくお願いいたします。

食べチョクは、2020年もお客様・生産者さまからご意見を頂戴しながら、日々サービスのカイゼンを行なって参ります。
早速、1月もたくさんのカイゼン行いましたので、いくつかカイゼン内容をご紹介いたします。

1月の改善サマリー

  • カイゼン数: 61 項目
    • 新機能: 40 項目
    • バグ改修: 18 項目
    • その他: 3 項目

上記の中から是非お伝えしたいカイゼン内容をご紹介いたします!

1. コンビニ決済が使用できるようになりました!

これまでお支払い方法はクレジットカード決済のみでしたが、1月よりコンビニ決済もご利用いただけるようになりました!

コンビニ決済で購入したい場合の手順を簡単にご説明いたします。

1. 注文画面にてお支払い方法からコンビニ払いをお選びください

f:id:vividgarden-tech:20200203180007j:plain:w300

2. コンビニ払いに必要な情報をご入力ください。

f:id:vividgarden-tech:20200203180250j:plain:w300

1月時点では、ご利用いただけるコンビニは、ローソン、ファミリーマート、ミニストップの3社となります。
今後、セブンイレブンも追加される予定です。

注文が完了すると、以下のようなページが表示されます。

f:id:vividgarden-tech:20200127191256j:plain:w300

3. お店でお支払い

注文が完了するとコンビニ決済依頼完了のお知らせという件名でメールが届きます。
メールの内容に従って、ご指定のコンビニからお支払いを行ってください。
※お支払い期限についてはメールに記載されております。

お支払いが完了できましたら、確認完了のメールをお送りさせていただきます。

コンビニ払いはこれまでも多くのご要望があった機能です。
是非、食べチョクの利用を見送っていたお客様にもご利用いただきたいと思います。

2. 生産者の新規登録画面のカイゼンを行いました

生産者の新規登録時は入力項目が多いため、生産者様が入力しやすいように細かいカイゼンを行っております。

  • 記述例の提示
    商品名や紹介文など各所にお客様に伝わりやすい記述例を具体的に提示し、生産者様がスムーズに入力できるようにしています。
  • 写真の例を提示
    プロフィール写真やこだわり写真についても、具体例を提示して、生産者様がスムーズに入力できるようにしています。
  • プレビュー画面の追加
    入力しながら販売開始をイメージできるようにプレビュー画面を追加しています。

3. よくあるご質問ページを作成しました

お客様からのお問い合わせの多い内容をまとめた、よくあるご質問のページを作成しております。

是非、お問い合わせの前に、よくあるご質問ページをチェックしてみてください。

www.tabechoku.com

最後に

今後もユーザーの皆様・生産者の皆様のお声を積極的に取り入れていきたいと考えております。

食べチョクトップページの下部にご意見フォームを設置しておりますので、なにか気になることがございましたら、ささいなことでもお伝えいただければ幸いです。

また、食べチョクではデザイナー・エンジニアを切に募集しています。
興味のある方は、是非Wantedlyよりご連絡ください!

www.wantedly.com

2019年11月のリリース内容をご紹介!

こんにちは、エンジニアの鹿倉です。

食べチョクでは、お客様、生産者さまからご意見を頂戴しながら、日々サービスのカイゼンを行なっております。
11月もたくさんのご意見を頂きカイゼンを行いました!

11月のカイゼンサマリーはこちらです。

11月の改善サマリー

  • カイゼン数: 80 項目
    • 新機能: 46 項目
    • バグ改修: 28 項目
    • その他: 6 項目

上記の中から是非お伝えしたいカイゼン内容をご紹介いたします!

1. 検索精度をカイゼンしました

今までのフリーワード検索は検索の対象が広域であるため、意図しない検索結果となることがありました。
例えば、「トマト」と検索した場合に、「メロン」の商品が表示されるといった具合です。
複数品目を栽培している生産者様の場合、商品説明文に他の品目についても記載されている場合があるため意図しない品目が引っかかってしまうというのが大きな理由でした。

以前は商品数が少なかったため、なるべくヒットしやすいように検索対象を広くしていましたが、商品数も増えてきて十分な結果を返せるようになってきたため、商品説明文を検索対象から外しています。

2. お問い合わせページと会員登録画面にてスパム対策を行いました

サービスの成長に伴い、ボットからの不正アクセスが増えてきたため、reCAPTCHAを導入しました。
ユーザの皆様に影響のある改善ではございませんが、セキュリティ向上のためにも今回の対策を行いました。

f:id:vividgarden-tech:20191209195107j:plain:w300

3. お歳暮特集をリリースしました!

お歳暮シーズンに向けて、特集ページをリリースしました。 ギフトとしても喜ばれることはもちろんですし、自分へのご褒美としてもいかがでしょうか!

www.tabechoku.com

私のオススメは椎茸です。肉厚で香り高く、1口食べただけで幸せになります。是非食べてみてください。

www.tabechoku.com

4. フルーツセレクトに関する投稿が出来る様になりました

10月にリリースしたフルーツセレクトですが、初回発送が11月ということもあり、実はまだ10月時点ではフルーツセレクトに関する投稿が出来ませんでした。
初回の発送に合わせて、フルーツセレクトに関する投稿も出来るようになっています。

すでに、ご投稿いただいているユーザ様もいます。

www.tabechoku.com

www.tabechoku.com

是非、フルーツセレクトをご利用の際はお客様の声を生産者に届けてみてください。

5. [生産者向けリリース]配送方法にクリックポストを追加しました

生産者からのご意見で、クリックポストで発送する場合は、通常の郵便扱いとなるためお届け日の指定が出来ないが、 システム上は、お届け日の登録が必須のため、発送完了が出来ない、または無理にお届け日を登録してもお客様に不適切なご案内になるとのご指摘を頂きました。

上記のご指摘を受けて配送方法(配送業者)にクリックポストを追加しております。
クリックポストを指定した場合もお届け日の入力はシステム上必要ではありますが、あくまで目安の扱いとなりますので、お客様にお届け日をご案内することはございません。

f:id:vividgarden-tech:20191209194156p:plain:w300

6. [生産者向けリリース]出荷リストをCSVダウンロード出来るようになりました

生産者から出荷リストをCSVでも扱えるようにしたいとのご意見がございました。
一覧ページの右上の操作ボタンからダウンロードが出来るように対応しています。

f:id:vividgarden-tech:20191209202413j:plain:w300

7. Rails6にアップデートしました!

直接お客様に関係あることではございませんが、食べチョクもようやくRails6へアップデートしました!!!

こちらについては別途ブログを書こうと思っています。

最後に

今後もユーザーの皆様・生産者の皆様のお声を積極的に取り入れていきたいと考えております。

食べチョクトップページの下部にご意見フォームを設置しておりますので、なにか気になることがございましたら、ささいなことでもお伝えいただければ幸いです。

また、食べチョクではデザイナー・エンジニアを切に募集しています。
興味のある方は、是非Wantedlyよりご連絡ください!

www.wantedly.com

www.wantedly.com

www.wantedly.com

新サービス「食べチョク フルーツセレクト」をリリース!2019年10月のカイゼン内容をご紹介!

こんにちは、食べチョクの鹿倉です。

食べチョクでは、お客様、生産者さまからご意見を頂戴しながら、日々サービスのカイゼンを行なっております。 10月もたくさんのご意見を頂きカイゼンを行いました!

10月のカイゼンサマリーはこちらです。

10月の改善サマリー

  • カイゼン数: 81 項目
    • 新機能: 40 項目
    • バグ改修: 22 項目
    • その他: 19 項目

上記の中から是非お伝えしたいカイゼン内容をご紹介いたします!

1. 新サービス「食べチョク フルーツセレクト」をリリースしました!!

www.tabechoku.com

サービス概要

「食べチョク フルーツセレクト」は毎月、旬の美味しい果物をお届けするサブスクリプションサービスです。

もちろん農家さんから直送です!!

ご契約プランは「1ヶ月」「3ヶ月」「6ヶ月」の3つのプランからお選びすることができ、契約期間が長くなるほど料金がお安くなる料金体系です。

下記の通り、6ヶ月プランだと1ヶ月プランに比べて3000円お得になります!

f:id:vividgarden-tech:20191106213430j:plain:w300

お届けする果物は毎月1日にメールにてご案内いたします。(マイページでもご確認頂けます)

そして、11月は北東農園さんの完熟極早生みかんを予定しております!

私も食べてみましたが、とっても美味しいので是非皆さんのも食べていただきたい一品です。

note.mu

美味しすぎてnoteも書いてますので、よろしければ是非ご覧ください。

2. 記事機能をリリースしました!!

www.tabechoku.com

運営からオススメの商品や情報をお届けする記事機能をリリースしています。

スタッフのオススメや人気商品まとめなど楽しい記事をどんどん配信していきますので是非チェックしてみてください!!

3. トップページに品目の一覧を追加しました!!

f:id:vividgarden-tech:20191107010439p:plain:w300

以前からサイドメニューに品目から探せるリンクを設置していましたが、スマホだとサイドメニューが閉じた状態のため気づきにくく、ユーザーの方々より品目から探せることを知らなかったというご意見を頂きました。

そこでトップページにも品目の一覧を追加してユーザーが分かりやすいようにしています。

4. [生産者向けカイゼン] 注文リストに並び替え条件&絞り込み機能を追加しました!!

これまで注文リストはお届け日順で並んでいるだけで、生産者様の作業に合わせて一覧を表示することが出来ていませんでした。

そのため、生産者様より注文数増加によりご注文の受付業務が煩雑になってきたとのご意見を頂きました。

そこで、生産者様の作業に合わせて注文リストを表示できるように並び替え条件の変更と絞り込み機能を追加しています。

f:id:vividgarden-tech:20191107170435p:plain:w300

並び替え条件

以下の条件で並び替えができるようになりました。

  • 注文日 昇順/降順
  • お届け日 降順/昇順
  • 商品名 降順/昇順

絞り込み機能

以下のステータスで絞り込みができるようになりました。

  • 注文未確定 ... まだ未確認の注文だけを表示します。
  • 発送準備中 ... 確認済みで発送待ちの注文だけを表示します。

最後に

今後もユーザーの皆様・生産者の皆様のお声を積極的に取り入れていきたいと考えております。

食べチョクトップページの下部にご意見フォームを設置しておりますので、なにか気になることがございましたら、ささいなことでもお伝えいただければ幸いです。

また、食べチョクではデザイナー・エンジニアを切に募集しています。
興味のある方は、是非Wantedlyよりご連絡ください!

www.wantedly.com

www.wantedly.com

www.wantedly.com