Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 279 lines (246 sloc) 10.282 kb
d036073 BossDB is now its own project
Evan Miller authored
1 %% @doc Chicago Boss database abstraction
2
3 -module(boss_db).
4
5 -export([start/1, stop/0]).
6
7 -export([
8 find/1,
9 find/2,
10 find/3,
11 find/4,
12 find/5,
13 find/6,
14 count/1,
15 count/2,
16 counter/1,
17 incr/1,
18 incr/2,
19 delete/1,
20 save_record/1,
21 push/0,
22 pop/0,
23 depth/0,
24 dump/0,
25 execute/1,
26 transaction/1,
27 validate_record/1,
28 type/1,
29 data_type/2]).
30
31 -define(DEFAULT_TIMEOUT, (30 * 1000)).
fd2df1d Manage connections with Poolboy
Evan Miller authored
32 -define(POOLNAME, boss_db_pool).
d036073 BossDB is now its own project
Evan Miller authored
33
34 start(Options) ->
fd2df1d Manage connections with Poolboy
Evan Miller authored
35 AdapterName = proplists:get_value(adapter, Options, mock),
36 Adapter = list_to_atom(lists:concat(["boss_db_adapter_", AdapterName])),
37 Adapter:init(Options),
38 lists:foldr(fun(ShardOptions, Acc) ->
39 case proplists:get_value(db_shard_models, ShardOptions, []) of
40 [] -> Acc;
41 _ ->
42 ShardAdapter = case proplists:get_value(db_adapter, ShardOptions) of
43 undefined -> Adapter;
44 ShortName -> list_to_atom(lists:concat(["boss_db_adapter_", ShortName]))
45 end,
46 ShardAdapter:init(ShardOptions ++ Options),
47 Acc
48 end
49 end, [], proplists:get_value(shards, Options, [])),
d036073 BossDB is now its own project
Evan Miller authored
50 boss_db_sup:start_link(Options).
51
52 stop() ->
53 ok.
54
a678c25 Fix transactions
Evan Miller authored
55 db_call(Msg) ->
56 case get(boss_db_transaction_info) of
57 undefined ->
58 boss_pool:call(?POOLNAME, Msg, ?DEFAULT_TIMEOUT);
59 State ->
60 {reply, Reply, _} = boss_db_controller:handle_call(Msg, self(), State),
61 Reply
62 end.
63
d036073 BossDB is now its own project
Evan Miller authored
64 %% @spec find(Id::string()) -> BossRecord | {error, Reason}
65 %% @doc Find a BossRecord with the specified `Id'.
66 find("") -> undefined;
67 find(Key) when is_list(Key) ->
a678c25 Fix transactions
Evan Miller authored
68 db_call({find, Key});
d036073 BossDB is now its own project
Evan Miller authored
69 find(_) ->
70 {error, invalid_id}.
71
72 %% @spec find(Type::atom(), Conditions) -> [ BossRecord ]
73 %% @doc Query for BossRecords. Returns all BossRecords of type
74 %% `Type' matching all of the given `Conditions'
75 find(Type, Conditions) ->
76 find(Type, Conditions, all).
77
78 %% @spec find(Type::atom(), Conditions, Max::integer() | all ) -> [ BossRecord ]
79 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
80 %% `Type' matching all of the given `Conditions'
81 find(Type, Conditions, Max) ->
82 find(Type, Conditions, Max, 0).
83
84 %% @spec find( Type::atom(), Conditions, Max::integer() | all, Skip::integer() ) -> [ BossRecord ]
85 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
86 %% `Type' matching all of the given `Conditions', skipping the first `Skip' results.
87 find(Type, Conditions, Max, Skip) ->
88 find(Type, Conditions, Max, Skip, id).
89
90 %% @spec find( Type::atom(), Conditions, Max::integer() | all, Skip::integer(), Sort::atom() ) -> [ BossRecord ]
91 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
92 %% `Type' matching all of the given `Conditions', skipping the
93 %% first `Skip' results, sorted on the attribute `Sort'.
94 find(Type, Conditions, Max, Skip, Sort) ->
95 find(Type, Conditions, Max, Skip, Sort, str_ascending).
96
97 %% @spec find( Type::atom(), Conditions, Max::integer() | all, Skip::integer(), Sort::atom(), SortOrder ) -> [ BossRecord ]
98 %% SortOrder = num_ascending | num_descending | str_ascending | str_descending
99 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
100 %% Type matching all of the given `Conditions', skipping the
101 %% first `Skip' results, sorted on the attribute `Sort'. `SortOrder' specifies whether
102 %% to treat values as strings or as numbers, and whether to sort ascending or
103 %% descending. (`SortOrder' = `num_ascending', `num_descending', `str_ascending', or
104 %% `str_descending')
105 %%
106 %% Note that Time attributes are stored internally as numbers, so you should
107 %% sort them numerically.
108
109 find(Type, Conditions, Max, Skip, Sort, SortOrder) ->
a678c25 Fix transactions
Evan Miller authored
110 db_call({find, Type, normalize_conditions(Conditions), Max, Skip, Sort, SortOrder}).
d036073 BossDB is now its own project
Evan Miller authored
111
112 %% @spec count( Type::atom() ) -> integer()
113 %% @doc Count the number of BossRecords of type `Type' in the database.
114 count(Type) ->
115 count(Type, []).
116
117 %% @spec count( Type::atom(), Conditions ) -> integer()
118 %% @doc Count the number of BossRecords of type `Type' in the database matching
119 %% all of the given `Conditions'.
120 count(Type, Conditions) ->
a678c25 Fix transactions
Evan Miller authored
121 db_call({count, Type, normalize_conditions(Conditions)}).
d036073 BossDB is now its own project
Evan Miller authored
122
123 %% @spec counter( Id::string() ) -> integer()
124 %% @doc Treat the record associated with `Id' as a counter and return its value.
125 %% Returns 0 if the record does not exist, so to reset a counter just use
126 %% "delete".
127 counter(Key) ->
a678c25 Fix transactions
Evan Miller authored
128 db_call({counter, Key}).
d036073 BossDB is now its own project
Evan Miller authored
129
130 %% @spec incr( Id::string() ) -> integer()
131 %% @doc Treat the record associated with `Id' as a counter and atomically increment its value by 1.
132 incr(Key) ->
133 incr(Key, 1).
134
135 %% @spec incr( Id::string(), Increment::integer() ) -> integer()
136 %% @doc Treat the record associated with `Id' as a counter and atomically increment its value by `Increment'.
137 incr(Key, Count) ->
a678c25 Fix transactions
Evan Miller authored
138 db_call({incr, Key, Count}).
d036073 BossDB is now its own project
Evan Miller authored
139
140 %% @spec delete( Id::string() ) -> ok | {error, Reason}
141 %% @doc Delete the BossRecord with the given `Id'.
142 delete(Key) ->
143 AboutToDelete = boss_db:find(Key),
144 case boss_record_lib:run_before_delete_hooks(AboutToDelete) of
145 ok ->
a678c25 Fix transactions
Evan Miller authored
146 Result = db_call({delete, Key}),
fd2df1d Manage connections with Poolboy
Evan Miller authored
147 case Result of
d036073 BossDB is now its own project
Evan Miller authored
148 ok ->
149 boss_news:deleted(Key, AboutToDelete:attributes()),
150 ok;
fd2df1d Manage connections with Poolboy
Evan Miller authored
151 _ ->
152 Result
d036073 BossDB is now its own project
Evan Miller authored
153 end;
154 {error, Reason} ->
155 {error, Reason}
156 end.
157
158 push() ->
a678c25 Fix transactions
Evan Miller authored
159 db_call(push).
d036073 BossDB is now its own project
Evan Miller authored
160
161 pop() ->
a678c25 Fix transactions
Evan Miller authored
162 db_call(pop).
d036073 BossDB is now its own project
Evan Miller authored
163
164 depth() ->
a678c25 Fix transactions
Evan Miller authored
165 db_call(depth).
d036073 BossDB is now its own project
Evan Miller authored
166
167 dump() ->
a678c25 Fix transactions
Evan Miller authored
168 db_call(dump).
d036073 BossDB is now its own project
Evan Miller authored
169
170 %% @spec execute( Commands::iolist() ) -> RetVal
171 %% @doc Execute raw database commands on SQL databases
172 execute(Commands) ->
a678c25 Fix transactions
Evan Miller authored
173 db_call({execute, Commands}).
d036073 BossDB is now its own project
Evan Miller authored
174
175 %% @spec transaction( TransactionFun::function() ) -> {atomic, Result} | {aborted, Reason}
176 %% @doc Execute a fun inside a transaction.
177 transaction(TransactionFun) ->
a678c25 Fix transactions
Evan Miller authored
178 Worker = poolboy:checkout(?POOLNAME),
179 State = gen_server:call(Worker, state, ?DEFAULT_TIMEOUT),
180 put(boss_db_transaction_info, State),
181 {reply, Reply, _} = boss_db_controller:handle_call({transaction, TransactionFun}, self(), State),
182 put(boss_db_transaction_info, undefined),
183 poolboy:checkin(?POOLNAME, Worker),
184 Reply.
d036073 BossDB is now its own project
Evan Miller authored
185
186 %% @spec save_record( BossRecord ) -> {ok, SavedBossRecord} | {error, [ErrorMessages]}
187 %% @doc Save (that is, create or update) the given BossRecord in the database.
188 %% Performs validation first; see `validate_record/1'.
189 save_record(Record) ->
190 case validate_record(Record) of
191 ok ->
192 RecordId = Record:id(),
193 {IsNew, OldRecord} = if
194 RecordId =:= 'id' ->
195 {true, Record};
196 true ->
197 case find(RecordId) of
198 {error, _Reason} -> {true, Record};
199 undefined -> {true, Record};
200 FoundOldRecord -> {false, FoundOldRecord}
201 end
202 end,
203 HookResult = case boss_record_lib:run_before_hooks(Record, IsNew) of
204 ok -> {ok, Record};
205 {ok, Record1} -> {ok, Record1};
206 {error, Reason} -> {error, Reason}
207 end,
208 case HookResult of
209 {ok, PossiblyModifiedRecord} ->
a678c25 Fix transactions
Evan Miller authored
210 case db_call({save_record, PossiblyModifiedRecord}) of
d036073 BossDB is now its own project
Evan Miller authored
211 {ok, SavedRecord} ->
212 boss_record_lib:run_after_hooks(OldRecord, SavedRecord, IsNew),
213 {ok, SavedRecord};
214 Err -> Err
215 end;
216 Err -> Err
217 end;
218 Err -> Err
219 end.
220
221 %% @spec validate_record( BossRecord ) -> ok | {error, [ErrorMessages]}
222 %% @doc Validate the given BossRecord without saving it in the database.
223 %% `ErrorMessages' are generated from the list of tests returned by the BossRecord's
224 %% `validation_tests/0' function (if defined). The returned list should consist of
225 %% `{TestFunction, ErrorMessage}' tuples, where `TestFunction' is a fun of arity 0
226 %% that returns `true' if the record is valid or `false' if it is invalid.
227 %% `ErrorMessage' should be a (constant) string which will be included in `ErrorMessages'
228 %% if the `TestFunction' returns `false' on this particular BossRecord.
229 validate_record(Record) ->
230 Type = element(1, Record),
231 Errors = case erlang:function_exported(Type, validation_tests, 1) of
232 true -> [String || {TestFun, String} <- Record:validation_tests(), not TestFun()];
233 false -> []
234 end,
235 case length(Errors) of
236 0 -> ok;
237 _ -> {error, Errors}
238 end.
239
240 %% @spec type( Id::string() ) -> Type::atom()
241 %% @doc Returns the type of the BossRecord with `Id', or `undefined' if the record does not exist.
242 type(Key) ->
243 case find(Key) of
244 undefined -> undefined;
245 Record -> element(1, Record)
246 end.
247
248 data_type(_, _Val) when is_float(_Val) ->
249 "float";
250 data_type(_, _Val) when is_binary(_Val) ->
251 "binary";
252 data_type(_, _Val) when is_integer(_Val) ->
253 "integer";
254 data_type(_, _Val) when is_tuple(_Val) ->
255 "datetime";
256 data_type(_, _Val) when is_boolean(_Val) ->
257 "boolean";
258 data_type(_, undefined) ->
259 "null";
260 data_type('id', _) ->
261 "id";
262 data_type(Key, Val) when is_list(Val) ->
263 case lists:suffix("_id", atom_to_list(Key)) of
264 true -> "foreign_id";
265 false -> "string"
266 end.
267
268 normalize_conditions(Conditions) ->
269 normalize_conditions(Conditions, []).
270
271 normalize_conditions([], Acc) ->
272 lists:reverse(Acc);
273 normalize_conditions([Key, Operator, Value|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
274 normalize_conditions(Rest, [{Key, Operator, Value}|Acc]);
275 normalize_conditions([{Key, Value}|Rest], Acc) when is_atom(Key) ->
276 normalize_conditions(Rest, [{Key, 'equals', Value}|Acc]);
277 normalize_conditions([{Key, Operator, Value}|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
278 normalize_conditions(Rest, [{Key, Operator, Value}|Acc]).
Something went wrong with that request. Please try again.