Subscribed unsubscribe Subscribe Subscribe

Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

Hash::Compactでキー名にエイリアスをふってストレージ容量を節約する

MySQLのカラムなりMemcached的なストレージなりに構造化されたデータをつっこみたい時に、長ったらしいキー名が何度も使われるのは容量の無駄だなあという話になって、簡単にキー名にaliasをふれるようにしたら便利だね、つか、Data::Modelに確かそういうのあったよね、汎用的に使えるようにしたい、とid:nanto_viと話したので、ちょっとやってみた。

たとえば、以下のようなデータがあるとして

{
    name => 'antipop',
    age  => 17,
    ...
}

これをMySQLのカラムなりMemcached的なストレージなりに、JSONだとかなんだとかでシリアライズして保存すると、データが大量になってくると、キー名(ここではname, age)の重複がもったいなくなる。そこで、

my $hash = Hash::Compare({
    name => 'antipop',
    age  => 17,
    ...
}, {
    name     => { alias_for => 'n' },
    age      => { alias_for => 'a' },
    location => { alias_for => 'l', default => 'kyoto' },
    ...
);

とかしておくと、paramメソッドによるget/setは、以下のように、引数でわたしたキー名でアクセスできるのだけど、

$hash->param('name');
$hash->param(age => 34);

to_hashメソッドが返す実際のhashは以下のようにalias_forで指定したキー名が使われる(alias_forが指定されていなかったら元のキー名)。

$hash->to_hash #=> { n => 'antipop', a => '34' }

さらに、defaultが指定されていると、そのキー名のvalueがない時には

  • getではそのdefaultの値が返る
  • setでは、defaultと同じvalueがわたされたら無駄なのでフィールドを削除する

という挙動になります。default値として決まった値なら、いちいちストレージに保存する必要はないので、hashから消しちゃいます。

具体的にmemcachedで使ってみる例が以下。空間コストが安くなってきてるとはいえ少しでも節約できればするに越したことはないわけで、うれしいですね。

酒飲みながら適当に書いたものなので、いろいろ修正するかも。

https://github.com/kentaro/perl-hash-compact/blob/master/eg/with_memcached.pl

#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;

package My::Memcached;
use strict;
use warnings;
use parent qw(Cache::Memcached::Fast);

use JSON;
use Hash::Compact;

my $options = {
    foo => {
        alias_for => 'f',
    },
    bar => {
        alias_for => 'b',
        default   => 'bar',
    },
};

sub get {
    my ($self, $key) = @_;
    my $value = $self->SUPER::get($key);
    Hash::Compact->new(decode_json $value, $options);
}

sub set {
    my ($self, $key, $value, $expire) = @_;
    my $hash = Hash::Compact->new($value, $options);
    $self->SUPER::set($key, encode_json $hash->to_hash, $expire);
}

package main;

my $key   = 'key';
my $value = { foo => 'foo' };
my $memd  = My::Memcached->new({servers => [qw(localhost:11211)]});
   $memd->set($key, $value);

my $cached_value = $memd->get($key);
is_deeply $cached_value->param('foo'), 'foo';
is_deeply $cached_value->param('bar'), 'bar';
is_deeply $cached_value->to_hash, +{ f => 'foo' };

$cached_value->param(bar => 'baz');
$memd->set($key, $cached_value->to_hash);

$cached_value = $memd->get($key);
is_deeply $cached_value->param('foo'), 'foo';
is_deeply $cached_value->param('bar'), 'baz';
is_deeply $cached_value->to_hash, +{ f => 'foo', b => 'baz' };

done_testing;