Google Docsみたいな共同編集Webサイトを作るライブラリ Yjs 初めの一歩 まずは動作させてみる

Yjs と聞いて、何を思い浮かべるでしょうか?
検索すると、〇〇先輩とか出てきますが、先輩ではありません。

↓これです。

A CRDT framework with a powerful abstraction of shared data
https://github.com/yjs/yjs/tree/master

CRDTできるフレームワークと書いてありますが、CRDTとは何かと言い出すと、Conflict-free replicated data typeの略で結構難しい話になります。
そこで、CRDTをすごく平たく言うと、Google Docsとか、MicrosoftのOffice365のExcelとか、AさんとBさんが同じExcelのシートを開いていても、共同で編集できますよね。あれがCRDTです。

そんな機能を実現させるためのライブラリです。

ベルリン在住のケビン・ヤーン(?)さんが作っているようです。
ありがとう、ケビンさん!

さて、まずはめっちゃ簡単に動作させてみます。

しかし、「Yjs sample」 とかググってみても、Reactを使ったりマイクラを使ったり、TODOアプリとかちょっと複雑なアプリが多く出てきます。
そういうのじゃなくって、「Hello World」的なことがまずは最初にしたい!という人いませんか?→はい、私です。
他にもそういう方がいるのではないかと思いまして、この記事を書きました。

灯台元暗しでYjsの公式サイトに「Hello World」的なサンプルがありました!

https://docs.yjs.dev/

import * as Y from 'yjs'

// Yjs documents are collections of
// shared objects that sync automatically.
const ydoc = new Y.Doc()
// Define a shared Y.Map instance
const ymap = ydoc.getMap()
ymap.set('keyA', 'valueA')

// Create another Yjs document (simulating a remote user)
// and create some conflicting changes
const ydocRemote = new Y.Doc()
const ymapRemote = ydocRemote.getMap()
ymapRemote.set('keyB', 'valueB')

// Merge changes from remote
const update = Y.encodeStateAsUpdate(ydocRemote)
Y.applyUpdate(ydoc, update)

// Observe that the changes have merged
console.log(ymap.toJSON()) // => { keyA: 'valueA', keyB: 'valueB' }

これを、index.mjs という名前で保存します。

PCにNode.jsはインストールされていますか?されていなければ、まずはNode.jsをインストールしてください。

Node.jsがインストールされていれば、次はYjsをインストールします。
私の開発環境はWindowsです。なのでコマンドプロンプトを開いて次のコマンドを実行します。

npm i yjs y-websocket

厳密にいうと、後半のy-websocketは今は必要なさそうですが、いずれ必要になりそうなので入れておきます。

インストールできましたね!

そしたら、さっき保存したindex.mjs をnodeコマンドで動作させます。

node index.mjs

すると、上記のスクショのように、サンプルコードの下記の部分が表示されます。

console.log(ymap.toJSON()) 
('keyA', 'valueA')

だったymap が 

('keyB', 'valueB')

という ymapRemote と統合されて

 { keyA: 'valueA', keyB: 'valueB' }

になったんですね~。

すごい!
もちろん、これだけではすごさは全く伝わらないと思うので、Yjsについては今後記事を書いていきたいと思います。

単純CASE式と検索CASE式

CASE式みたいな処理って、自分の場合はこれまでアプリケーション内で行うことが多くてMySQLで行うことが少なかったです。
しかし、使う機会があって若干はまったので、備忘録として残します。

やりたかったことは、「NULLか空文字だったら値を0として取得する」ことでした。
CASE式には単純CASE式と検索CASE式があってそれが頭に入っておらず、はまっておりました。

サンプルテーブルを以下に用意します。

START TRANSACTION;
    CREATE TABLE member (
        id int(11) NOT NULL,
        name varchar(64) NOT NULL,
        gender enum('male','female') NOT NULL,
        age int(11) NOT NULL,
        memo mediumtext,
        created datetime NOT NULL,
        updated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    ALTER TABLE member
        ADD PRIMARY KEY (id),
        ADD KEY gender (gender),
        ADD KEY age (age);

    ALTER TABLE member
        MODIFY id int(11) NOT NULL AUTO_INCREMENT;

    INSERT INTO member (name, gender, age, memo, created) VALUES
        ('suzuki', 'male', 25, 'イケメン', '2021-04-01 00:00:00'),
        ('ohtani', 'female', 30, NULL, '2021-05-01 00:00:00'),
        ('sato', 'female', 35, '', '2021-06-01 00:00:00'),
        ('nakamura', 'female', 40, 'おもしろい', '2021-07-01 00:00:00'),
        ('tanaka', 'female', 45, NULL, '2021-08-01 00:00:00'),
        ('inoue', 'male', 50, '博識', '2021-09-01 00:00:00'),
        ('ishida', 'female', 55, '優しい', '2021-10-01 00:00:00'),
        ('matsumoto',  'male', 60, '穏やか', '2021-11-01 00:00:00'),
        ('sasaki', 'male', 65, '', '2021-12-01 00:00:00'),
        ('kato', 'female', 70, '', '2022-01-01 00:00:00');
COMMIT;

テーブルの中身はざっとこんな感じ。

idnamegenderagememocreatedupdated
1suzukimale25イケメン2021-04-01 00:00:002022-05-13 14:18:42
2ohtanifemale30NULL2021-05-01 00:00:002022-05-13 14:18:42
3satofemale352021-06-01 00:00:002022-05-13 14:18:42
4nakamurafemale40おもしろい2021-07-01 00:00:00 2022-05-13 14:18:42
5tanakafemale45NULL2021-08-01 00:00:002022-05-13 14:18:42
6inouemale50博識2021-09-01 00:00:00 2022-05-13 14:18:42
7ishidafemale55優しい2021-10-01 00:00:002022-05-13 14:18:42
8matsumotomale60穏やか2021-11-01 00:00:002022-05-13 14:18:42
9sasakimale652021-12-01 00:00:002022-05-13 14:18:42
10katofemale702022-01-01 00:00:002022-05-13 14:18:42

性別が英語で入っているので日本語で取得してみたいと思います。
比較しやすいように名前と英語での性別も取得します。
以下のようにCASEのすぐ後ろにカラム名が来て、そのWHENの値と同値のCASE式を単純CASE式と呼ぶそうです。

# 単純CASE式
SELECT
    name,
    gender,
    CASE gender
        WHEN 'male' THEN '男性'
        WHEN 'female' THEN '女性'
    END AS gender_ja
FROM member;

検索結果

namegendergender_ja
suzukimale男性
ohtanifemale女性
satofemale女性
nakamurafemale女性
tanakafemale女性
inouemale男性
ishidafemale女性
matsumotomale男性
sasakimale男性
katofemale女性

maleの場合は「男性」、femaleの場合は「女性」と表示されます。

次は
60歳以上であれば「senior」
40歳以上60歳未満であれば「middle-age」
40歳未満を「young」
と出力するようにします。

検索CASE式ではWHENの後に検索条件が入ります。
CASEの後に列名は必要ありません。

# 検索CASE式
SELECT
    name,
    age,
    CASE 
        WHEN age >= 60 THEN 'senior'
        WHEN age >= 40 && age < 60 THEN 'middle-age'
        ELSE 'young'
    END AS generation
FROM member;

検索結果

nameagegeneration
suzuki25young
ohtani30young
sato35young
nakamura40middle-age
tanaka45middle-age
inoue50middle-age
ishida55middle-age
matsumoto60senior
sasaki65senior
kato70senior

応用が効くのは検索CASE式で単純CASE式は、検索CASE式でも行うことが出来ます。

最初に行った性別を日本語で表示する単純CASE式を検索CASE式で書いてみます。

# 検索CASE式
# 結果は二つ上の表と同じ
SELECT
    name,
    gender,
    CASE
        WHEN gender LIKE 'male' THEN '男性'
        WHEN gender LIKE 'female' THEN '女性'
    END AS gender_ja
FROM member;

今回やりたかったことは、「NULLか空文字だったら値を0として取得する」だったので、検索CASE式を使えば簡単に出来そうです。
CASEの後ろに memo と書いていたので若干はまっていました。

SELECT
    name,
    memo,
    CASE
        WHEN (memo IS NULL || memo = '') THEN 0
        ELSE 1
    END AS memo_exists
FROM member;

検索結果

namememomemo_exists
suzukiイケメン1
ohtaniNULL0
sato0
nakamuraおもしろい1
tanakaNULL0
inoue博識1
ishida優しい1
matsumoto穏やか1
sasaki0
kato0

めでたしです。
と、最初は上のSQLを書いたのですがCHAR_LENGTHを使えば空文字判定出来そうですね…。
TRIMも必要なら一緒にすれば良さそうです(全角スペースは除かれないので注意)。

SELECT
    name,
    memo,
    CASE
        WHEN CHAR_LENGTH(memo) > 0 THEN 1
        ELSE 0
    END AS memo_exists
FROM member;

上のIF文を使って書くことも可能です。

SELECT
    name,
    memo,
    IF(CHAR_LENGTH(memo) > 0, 1, 0) AS memo_exists
FROM member;

単純CASEで書けるところも検索CASE式で書けるので、検索CASE式 → 単純CASE式の順に学んだ方が覚えやすい気がします。

SourceTreeでAccessTokenを作ってGithubに接続できない

SourceTree Ver 3.3

実は半年ぐらい、下記のエラーが出て

git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks fetch --no-tags origin
remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.
remote: Please see https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/ for more information.
fatal: Authentication failed for 'https://github.com/hogehoge'

SourceTreeでGithubに接続できなくて困っていました。

Githubがメールアカウントとパスワードではなく、Access Tokenというものを使ってしかGihutbクライアントからアクセスできなくなったのは有名な話ですが、下記などいろいろなサイトでやり方が紹介されているものの、

https://zenn.dev/koushikagawa/articles/3c35e503c8553a

https://syslog.life/2022/01/21/github-sourcetree-alignment-access-token/

上記のサイトのようにOauthにしてパスワードをトークンにしても、ベーシック認証にしてパスワードをアクセストークンにするなど、いろいろやってみてもダメでした。

SourceTreeをアンインストールしたりしてもダメだったので

「どうせ僕なんて…アクセストークンも使いこなせない人間なんだ…。」

とウジウジしながらコマンドプロンプトや他のGihutbクライアントを使う毎日でした。(>_<)

しかし、今日やっと解決しましたので、書いておきます!

こちら、SourceTree本家のサイトに書いてありましたね💦

Sourcetree ignores github token and throws 403 error

https://community.atlassian.com/t5/Sourcetree-questions/Sourcetree-ignores-github-token-and-throws-403-error/qaq-p/1778978#U1785059

ちゃんと英語のサイト読めよってことですね!

同じようなことに困っている方がいらっしゃると思うので、以下、やり方を書いておきます。

①右上の設定ボタンをクリックし、下記の「リモート」を表示させて、編集をクリックします。

②次の画面で、リモートの詳細設定という設定がありますが、ここのURL/パス欄に

次のように入れます。

https://<token>@<git_url>.git 

わかりづらいので解説すると、token が 123456 だとして、GithubのURLが https://github.com/hogehoge/hogeだとすると、

https://123456@github.com/hogehoge/hoge.git

と入れるということです。

すると、何と接続ができます。

ヤッター!!

新規のCloneするときも、

一番最初の 「元のパス/URL」 とある欄に、同じようにアクセストークンを含めたURLを入力します。

すると、接続できます。

ありがとう、世界!⊂(^-^)⊃ 吾妻山公園というところからの眺めです。

jest で vue のコンポーネントのメソッドをテストしたいが呼び出せない

jest 27.4.3
vue/test-utils 1.3.0
vue 2.5.2

すみませんが、jest 初心者です。

初心者らしく、初心者がハマりそうなポイントを書いておきます。

TimeTable.vue というコンポーネントがありまして、次のようにコンポーネントのテストを作ったりすると思いますが

/**
 * TimeTable.spec.js
 * @jest-environment jsdom
 */

import {createLocalVue, mount} from '@vue/test-utils'
import TimeTableVue from '../../../../../components/timeTable/templates/TimeTable'
import Vuex from 'vuex'  

 const localVue = createLocalVue()
  localVue.use(Vuex)

  let store

  const wrapper = mount(TimeTableVue, {
    stubs: ['font-awesome-icon'],
    propsData: {
      getServiceHandler: jest.fn(),
    },
    components: {},
    computed: {},
    store,
    localVue
  })

describe('text check', () => {
  it('should be some class', () => {
    expect(wrapper.find('.some_class').exists()).toBe(true)
  })
})

ここで、TimeTable.vue のメソッドに

// 略  
methods: {
    setDrivers (drivers) {
      this.drivers = drivers
    },
// 略

というメソッドがあった場合、テスト(TimeTable.spec.js)から

wrapper.setDrivers(drivers)

とやっても、

TypeError: setDrivers is not a function

みたいにメソッドが見つからないエラーになってしまいます。

これ、Vueのコンポーネントにアクセスする場合は、

wrapper.vm

を使わないとダメなんですね。

つまり、TimeTable.vue コンポーネントの中の setDrivers() にアクセスしたい場合は

wrapper.vm.setDrivers()

としないとダメです。

Vue Test Utilsの公式サイトのwrapperの説明に下記のようにありました。
https://v1.test-utils.vuejs.org/api/wrapper/#properties

———————————————————————-
# vm

Component (read-only): This is the Vue instance. You can access all the instance methods and properties of a vm

with wrapper.vm. This only exists on Vue component wrapper or HTMLElement binding Vue component wrapper.

———————————————————————-

findとか使う場合は wrapper 自体を使うので、間違えちゃいますよね!
(๑>◡<๑)

大量のレコードをDELETEしたならOPTIMIZE TABLEをしようというお話

そもそも大量にDELETEしない方が安心ですが…。
LIMITをつけて刻んだDELETEでも同様です。
DBのストレージ容量の肥大化を防ぐため、ログテーブルなどのもう利用しない古いレコードを大量に物理削除したいときとかってあります。
物理削除した場合は、

OPTIMIZE TABLE table_name;

を実行しないとディスク領域が解放されません。
ただし、実行した場合はテーブルロックがかかります。
サービスがリアルタイムで稼働中の場合は、実行タイミングに注意が必要です。

試しに実行してみます。
以下のような適当なサンプルテーブルを用意してみます。

CREATE TABLE people (
    id int(11) NOT NULL,
    name varchar(64) NOT NULL,
    height int(11) NOT NULL,
    weight int(11) NOT NULL,
    gender enum('male','female') NOT NULL,
    updated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE people
    ADD PRIMARY KEY (id),
    ADD KEY height (height),
    ADD KEY weight (weight);

ALTER TABLE people
    MODIFY id int(11) NOT NULL AUTO_INCREMENT;

まず、10行入れます。

INSERT INTO people (name, height, weight, gender) VALUES
 ('suzuki', 170, 65, 'male'),
 ('ohtani', 190, 90, 'male'),
 ('sato', 150, 45, 'female'),
 ('nakamura', 180, 75, 'male'),
 ('tanaka', 160, 55, 'female'),
 ('inoue', 155, 50, 'female'),
 ('ishida', 160, 60, 'male'),
 ('matsumoto', 165, 60, 'female'),
 ('sasaki', 175, 70, 'male'),
 ('kato', 145, 40, 'female');

10000010行に増やします。
自分の環境では2分30秒ぐらいかかりました。

INSERT INTO people (name, height, weight, gender)
SELECT p1.name, p1.height, p1.weight, p1.gender
FROM people p1, people p2, people p3, people p4, people p5, people p6, people p7;

ここで容量を確認しておきます。

SELECT table_name,
       table_rows AS rows,
       floor((data_length+index_length) / 1024 / 1024) AS all_mb,
       floor(data_length / 1024 / 1024) AS data_mb,
       floor(index_length / 1024 / 1024) AS index_mb
FROM information_schema.tables
WHERE table_schema = database()
ORDER BY (data_length + index_length) DESC;
table_name rows all_mb data_mb index_mb
people 9739621 660 406 253

およそ660MBになっています。
DELETE文でおよそ900万行削除したいと思います。
時間かかるのでブレイクタイムにすることをオススメします。
実行したら自分の環境では12分でした。

DELETE FROM people WHERE id > 1000000;

消し終えたらもう一回、容量を確認します。

table_name rows all_mb data_mb index_mb
people 1191489 660 406 253

行数は変わったけど容量は変わってないんですよね…。
そこで最適化します。

OPTIMIZE TABLE people;

もしくは

ALTER TABLE people ENGINE INNODB;

14秒くらいかかりました。

table_name rows all_mb data_mb index_mb
people 997458 81 50 31

減りましたのでめでたしです。
しかし、DELETE文が遅いのとテーブルがロックされるのが嫌がられるのがわかりますね。
一時テーブルを作成する方法が取られるのもこちらの結果を考えると納得がいきます。

参考サイト