Worker
Worker

Reputation: 2409

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

Upvotes: 6

Views: 1109

Answers (4)

fycth
fycth

Reputation: 3489

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.

Upvotes: 1

alavrik
alavrik

Reputation: 2161

You could use some extensible data serialization format such as JSON or Google Protocol Buffers.

Both of these formats support adding new fields without breaking backwards compatibility. By using them you won't need to introduce explicit versioning to your serialized data structures.

Choosing between the two formats depends on your use case. For instance, using Protocol Buffers is more reliable, whereas JSON is easier to get started with.

Upvotes: 0

Muzaaya Joshua
Muzaaya Joshua

Reputation: 7836

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.

Upvotes: 1

Hynek -Pichi- Vychodil
Hynek -Pichi- Vychodil

Reputation: 26121

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.

Upvotes: 1

Related Questions