Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best strategy for "mutable" records in Erlang

I develop the system where I assume will be many users. Each user has a profile represented inside the application as a record. To store user's profile I do the following base64:encode_to_string(term_to_binary(Profile)), so basically profiles stored in serialized maner.

So far everything is just fine. Now comes the question:

From time to time I do plan to extend profile functionality by adding and removing certain fields in it. My question is what is a best strategy to handle these changes in the code?

The approach I see at the moment is to do something like this:

Profile = get_profile(UserName),
case is_record(Profile, #profile1) of
    true ->
        % do stuff with Profile#profile1
        ok;
    _ ->
        next
end,
case is_record(Profile, #profile2) of
    true ->
        % do stuff with Profile#profile2
        ok;
    _ ->
        next
end,

I want to know if there are any better solutions for my task?

Additional info: I use is simple KV storage. It cannot store Erlang types this is why I use State#state.player#player.chips#chips.br

like image 834
Worker Avatar asked Nov 03 '11 06:11

Worker


3 Answers

Perhaps, you could use proplists.

Assume, you have stored some user profile.

User = [{name,"John"},{surname,"Dow"}].
store_profile(User).

Then, after a couple of years you decided to extend user profile with user's age.

User = [{name,"John"},{surname,"Dow"},{age,23}]. 
store_profile(User).

Now you need to get a user profile from DB

get_val(Key,Profile) ->
   V = lists:keyfind(Key,1,Profile),
   case V of
      {_,Val} -> Val;
      _ -> undefined
   end.

User = get_profile().
UserName = get_val(name,User).
UserAge = get_val(age,User).

If you get a user profile of 'version 2', you will get an actual age (23 in this particular case).

If you get a user profile of 'version 1' ('old' one), you will get 'undefined' as an age, - and then you can update the profile and store it with the new value, so it will be 'new version' entity.

So, no version conflict.

Probably, this is not the best way to do, but it might be a solution in some case.

like image 63
fycth Avatar answered Nov 05 '22 11:11

fycth


It strongly depend of proportion of number of records, frequency of changes and acceptable outage. I would prefer upgrade of profiles to newest version first due maintainability. You also can make system which will upgrade on-fly as mnesia does. And finally there is possibility keep code for all versions which I would definitely not prefer. It is maintenance nightmare.

Anyway when is_record/2 is allowed in guards I would prefer

case Profile of
    X when is_record(X, profile1) ->
        % do stuff with Profile#profile1
        ok;
    X when is_record(X, profile2) -> 
        % do stuff with Profile#profile2
        ok
end

Notice there is not catch all clause because what you would do with unknown record type? It is error so fail fast!

You have many other options e.g. hack like:

case element(1,Profile) of
    profile1 ->
        % do stuff with Profile#profile1
        ok;
    profile2 -> 
        % do stuff with Profile#profile2
        ok
end

or something like

{_, F} = lists:keyfind({element(1,Profile), size(Profile)},
    [{{profile1, record_info(size, profile1)}, fun foo:bar/1},
     {{profile2, record_info(size, profile2)}, fun foo:baz/1}]),
F(Profile).

and many other possibilities.

like image 25
Hynek -Pichi- Vychodil Avatar answered Nov 05 '22 10:11

Hynek -Pichi- Vychodil


The best approach is to have the copy of the serialized (profile) and also a copy of the same but in record form. Then , each time changes are made to the record-form profile, changes are also made to the serialized profile of the same user ATOMICALLY (within the same transaction!). The code that modifies the users record profile, should always recompute the new serialized form which, to you, is the external representation of the users record

-record(record_prof,{name,age,sex}).
-record(myuser,{
            username,
            record_profile = #record_prof{},
            serialized_profile
        }).
change_profile(Username,age,NewValue)-> %% transaction starts here.... [MyUser] = mnesia:read({myuser,Username}), Rec = MyUser#myuser.record_profile, NewRec = Rec#record_prof{age = NewValue}, NewSerialised = serialise_profile(NewRec), NewUser = MyUser#myuser{ record_profile = NewRec, serialized_profile = NewSerialised }, write_back(NewUser), %% transaction ends here..... ok.
So whatever the serialize function is doing, that's that. But this always leaves an overhead free profile change. We thereby keep the serialized profile as always the correct representation of the record profile at all times. When changes occur to the record profile, the serialized form must also be recomputed (transactional) so as to have integrity.
like image 1
Muzaaya Joshua Avatar answered Nov 05 '22 11:11

Muzaaya Joshua