-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathREADME.html
More file actions
449 lines (375 loc) · 21.1 KB
/
README.html
File metadata and controls
449 lines (375 loc) · 21.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<style>
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
body {
font-family: "Segoe WPC", "Segoe UI", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback";
font-size: 14px;
padding-left: 12px;
line-height: 22px;
}
img {
max-width: 100%;
max-height: 100%;
}
a {
color: #4080D0;
text-decoration: none;
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
h1, h2, h3 {
font-weight: normal;
}
a:hover {
color: #4080D0;
text-decoration: underline;
}
table {
border-collapse: collapse;
}
table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
table > thead > tr > th,
table > thead > tr > td,
table > tbody > tr > th,
table > tbody > tr > td {
padding: 5px 10px;
}
table > tbody > tr + tr > td {
border-top: 1px solid;
}
blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left: 5px solid;
}
code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 14px;
line-height: 19px;
}
.mac code {
font-size: 12px;
line-height: 18px;
}
code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
/** Theming */
.vscode-light {
color: rgb(30, 30, 30);
}
.vscode-dark {
color: #DDD;
}
.vscode-high-contrast {
color: white;
}
.vscode-light code {
color: #A31515;
}
.vscode-dark code {
color: #D7BA7D;
}
.vscode-light code > div {
background-color: rgba(220, 220, 220, 0.4);
}
.vscode-dark code > div {
background-color: rgba(10, 10, 10, 0.4);
}
.vscode-high-contrast code > div {
background-color: rgb(0, 0, 0);
}
.vscode-high-contrast h1 {
border-color: rgb(0, 0, 0);
}
.vscode-light table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vscode-dark table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
.vscode-light h1,
.vscode-light hr,
.vscode-light table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vscode-dark h1,
.vscode-dark hr,
.vscode-dark table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}
.vscode-light blockquote,
.vscode-dark blockquote {
background: rgba(127, 127, 127, 0.1);
border-color: rgba(0, 122, 204, 0.5);
}
.vscode-high-contrast blockquote {
background: transparent;
border-color: #fff;
}
</style>
<style>
/* Tomorrow Theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
</style>
<style>
/*
* Markdown PDF CSS
*/
pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
border-radius: 3px;
overflow-x: auto;
white-space: pre-wrap;
padding: 10px;
overflow-wrap: break-word;
}
blockquote {
background: rgba(127, 127, 127, 0.1);
border-color: rgba(0, 122, 204, 0.5);
}
.emoji {
height: 1.4em;
}
:not(pre):not(.hljs) > code {
color: #A31515;
font-size: inherit;
font-family: inherit;
}
</style>
</head>
<body>
<h1>Marten - document database and event store</h1>
<p>Document databases became popular in last years. They have a lot of places where they are suitable to use. RavenDb and many others are doing very well on production sites. This time I`ll tell something about Marten which has feature of document database and event store. The new thing is that Marten works on the top of Postgres database.</p>
<h4>What is it Marten?</h4>
<p>I`ve heard the first time about Marten on Slack channel when some person shared the link to <a href="https://jeremydmiller.com/2016/08/18/moving-from-ravendb-to-marten/">this post</a>. <a href="https://jeremydmiller.com/">Jeremy Miller</a> shows why his team decided to abandon RavenDb and setup completly new project with utilizing Postgres relational database. Marten just works similar to ORM on Postgres and provides functionality of document database and event store. It is great advantage when single relational database can be used in many ways in the project. But as always, if something is for many things then is the best in nothing.</p>
<p>Marten uses <a href="https://www.postgresql.org/docs/9.4/static/datatype-json.html">JSONB</a> Postgres type to store documents as default. It gives huge efficiency profits in comparision to normal JSON format. JSONB supports <a href="https://www.postgresql.org/docs/9.1/static/textsearch-indexes.html">GIN</a> indexing with the following <a href="https://www.postgresql.org/docs/9.4/static/functions-json.html#FUNCTIONS-JSONB-OP-TABLE">operators</a>. Postgres gives also possibility to use <a href="https://blog.lateral.io/2015/05/full-text-search-in-milliseconds-with-postgresql/">full-text search</a> within JSON documents.</p>
<h4>Marten as a document database</h4>
<p>Document database API is very similar to RavenDB what creators of the Marten say openly. They have created Marten as a substitution of RavenDB and that is reason of doing that.</p>
<p>To start using Marten it is needed to create <code>DocumentStore</code> object with connection string to Postgres database.</p>
<pre class="hljs"><code>
<span class="hljs-keyword">var</span> documentStore = DocumentStore.For(<span class="hljs-string">"connection string"</span>);
<span class="hljs-keyword">var</span> documentStore = DocumentStore.For(storeOptions =>
{
storeOptions.Connection(<span class="hljs-string">"connection string"</span>);
<span class="hljs-comment">// the rest of configuration</span>
});
</code></pre>
<p>Static <code>DocumentStore</code> class provides two overrides of <code>For(..)</code> method. First one is the simplest one and takes only connection string. Second override is more advanced and gives possibility of additonal configuration using <a href="https://github.com/JasperFx/marten/blob/master/src/Marten/StoreOptions.cs">StoreOptions</a> class.</p>
<p>By default Marten creates all relational database schemas automatically. It is very useful during development when you don`t want to care about migrations but may be harmful in production environment.</p>
<p>Marten provides a <a href="http://jasperfx.github.io/marten/documentation/cli/">tool</a> for managing database scheme. It is a nuget package available to use to apply, verify or generate current configuration of Marten documents into SQL database. It also provides functionality of generating up / down SQL scripts to modify database which can be used in any SQL migrations tool.</p>
<p>Marten creates separate tables for each document type. Tables are named using given pattern: <code>mt_doc_documentType</code> where <code>documentType</code> is the name of the document class. It is possible to override this naming by using attribute or explicit declaration in document store configuration.</p>
<p>Some people may disagree with the sentence that Marten works like ORM but in my opinion it does. Each object type is stored in separate table and each row in the table represents specified object instance. Marten maps the JSON field inside the row into instance of some type. Like in other ORMs it is possible to ovveride default mapping convention. In Marten it is possible by using attributes on class or fluent API.</p>
<pre class="hljs"><code>
[DocumentAlias(<span class="hljs-string">"anotheralias)]
public class User
{
}
DocumentStore.For(storeOptions =>
{
storeOptions.Schema.For<User>().DocumentAlias("</span>anotheralias);
});
</code></pre>
<h4>Database session</h4>
<p>It is possible to open modification database session in three different types of mode:</p>
<ul>
<li><code>_documentStore.LightweightSession()</code></li>
</ul>
<p>Session with manually document change tracking. To save or update document in the database <code>_session.Store(document)</code> must be called. It does not use <a href="http://jasperfx.github.io/marten/documentation/documents/advanced/identitymap/">IdentityMap</a> mechanism but it must be noted that it would be applided to all documents loaded using <code>IDocumentSession.Query<T>()</code> in this session.</p>
<ul>
<li><code>_documentStore.OpenSession()</code></li>
</ul>
<p>Session with manually document change tracking. It uses IdentityMap by default.</p>
<ul>
<li><code>_documentStore.DirtyTrackedSession()</code></li>
</ul>
<p>Session with automatically document change tracking. Change in every document loaded to the session would be detected and then <code>_session.Store(document)</code> would be invoked automatically. Dirty tracking session is done by comparising JSON documents node by node so enabling it would influence on the performance significantly.</p>
<h4>Session listeners</h4>
<p>It is possible to add custom session listeners which can intercept specified action during session unit of work. Such listener must implement <code>IDocumentSessionListener</code> interface and must be added during <code>DocumentStore</code> configuration. <code>DocumentSessionListenerBase</code> is a abstract class which can be used to override specified behaviour. It is not need to implement all listener actions.</p>
<pre class="hljs"><code><span class="hljs-keyword">var</span> listener = <span class="hljs-keyword">new</span> CustomListener();
<span class="hljs-keyword">var</span> store = DocumentStore.For(storeOptions =>
{
storeOptions.Listeners.Add(listener);
});
</code></pre>
<h4>Transaction isolation level</h4>
<p>It is possible to determine transaction isolation level in all above session modes. Default level is <code>ReadCommitted</code> but it can be set during opening the session e.g. <code>_store.DirtyTrackedSession(IsolationLevel.Serializable)</code>.</p>
<h4>Read-only database session</h4>
<p>There is also separate session which was designed only to access database in read-only mode. To create it, it is needed to call <code>_documentStore.QuerySession()</code>. Regarding document cache it works the same as in <code>_documentStore.LightweightSession()</code>.</p>
<h4>Optimistic concurrency</h4>
<p>Marten gives possibility to enable optimistic concurrency feature on specified document type. After enabling it, any document change which would tried to persist is checked firstly if any other change has been done since document was loaded into cache. Such change is detected by comparing version number located in metadata. It is also possible to store document with manually specified version. More information about it can be found in <a href="http://jasperfx.github.io/marten/documentation/documents/advanced/optimistic_concurrency/">documentation</a>.</p>
<h4>Querying</h4>
<p>Marten supports <a href="http://jasperfx.github.io/marten/documentation/documents/querying/linq/">synchronous</a> and <a href="http://jasperfx.github.io/marten/documentation/documents/querying/async/">asynchronous</a> linq queries quite well. It includes e.g. searching in child collections, deep queries, distinct, compiled queries, ordering, paging, select many and document value projections. Interesting thing regards paging (using <code>Take()</code>, <code>Skip()</code>) is that it is possible to get total count of all records in single query using <a href="http://jasperfx.github.io/marten/documentation/documents/querying/paging/">Stats()</a> extension. I think that this functionality was influenced by RavenDb which has similar <a href="https://ravendb.net/docs/article-page/3.5/csharp/client-api/session/querying/how-to-get-query-statistics">one</a>.</p>
<pre class="hljs"><code>QueryStatistics statistics;
<span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> _session.Query<User>()
.Stats(<span class="hljs-keyword">out</span> statistics)
.ToListAsync();
<span class="hljs-keyword">var</span> totalCount = statistics.TotalResults;
</code></pre>
<p>As other ORMs, Marten gives possibility to use Postgres SQL in queries, including queries with parameters.</p>
<pre class="hljs"><code><span class="hljs-keyword">var</span> user = session
.Query<User>(<span class="hljs-string">"where data ->> 'FirstName' = :FirstName and data ->> 'LastName' = :LastName"</span>,
<span class="hljs-keyword">new</span> { FirstName = <span class="hljs-string">"Jeremy"</span>, LastName = <span class="hljs-string">"Miller"</span>})
.Single();
</code></pre>
<p>Marten has possibility to get multiple documents in single query. <code>Include()</code> linq extension uses SQL join under the hood to achieve that. As default it uses inner join but it is possible to change it explicitly. <code>Include()</code> construction works in similar way as in RavenDb. It is possible to get single document or documents list.</p>
<pre class="hljs"><code><span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> session = _documentStore.LightweightSession())
{
Company company = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> session.Query<User>()
.Include<Company>(user => user.CompanyId2, comp => company = comp)
.SingleAsync(user => user.Id == id);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> UserWithCompany
{
Company = company,
User = result
};
}
</code></pre>
<h4>Batch queries</h4>
<p>It is possible to define set of queries which would be executed in single database call. There are similar soultions in other ORMs which behave like this but many of them work in implicit way (like nHibernate <code>ToFuture(..)</code>). Marten do more less the same but gives explicit way of defining such queries. First it is needed to get instance of <code>IBatchedQuery</code> from the session and then define the queries which should be done in it.</p>
<pre class="hljs"><code><span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> session = _documentStore.LightweightSession())
{
<span class="hljs-keyword">var</span> batch = session.CreateBatchQuery();
<span class="hljs-keyword">var</span> userPromise = batch.Load<User>(id);
<span class="hljs-keyword">var</span> usersPromise = batch.Query<User>().Where(u => u.FirstName.StartsWith(<span class="hljs-string">"Name"</span>)).ToList();
<span class="hljs-keyword">await</span> batch.Execute();
<span class="hljs-keyword">var</span> user = <span class="hljs-keyword">await</span> userPromise;
<span class="hljs-keyword">var</span> users = <span class="hljs-keyword">await</span> usersPromise;
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BatchUsers
{
User = user,
Users = users.ToList()
};
}
</code></pre>
<p>All queries defined within the batch will return the type <code>Task<TResult></code>. Important thing is that the result of this task can be get only after the batch has been executed.</p>
<h4>Document hierarchies</h4>
<p>Document hierarchies mechanism gives possibility to define inheritance between documents to query them separately. To achieve that it is needed to define appriopriate scheme definition during initiating <code>DocumentStore</code> instance. Martren supports one level hierarchies and multi level hierarchies. Both hierarchies types are being configured in similar way.</p>
<pre class="hljs"><code><span class="hljs-comment">// one level</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">GeneralUser</span> { }
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Employee</span> : <span class="hljs-title">GeneralUser</span> {}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Administrator</span> : <span class="hljs-title">GeneralUser</span> {}
<span class="hljs-comment">// multi level</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IVehicle</span> {}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Car</span> : <span class="hljs-title">IVehicle</span> {}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Toyota</span> : <span class="hljs-title">Car</span> {}
<span class="hljs-keyword">var</span> documentStore = DocumentStore.For(storeOptions =>
{
storeOptions.Schema.For<GeneralUser>()
.AddSubClass<Employee>()
.AddSubClass(<span class="hljs-keyword">typeof</span> (Administrator));
storeOptions.Schema.For<IVehicle>()
.AddSubClassHierarchy(
<span class="hljs-keyword">typeof</span>(Car),
<span class="hljs-keyword">typeof</span>(Toyota)
)
});
</code></pre>
<p>Table is created for the most general type. It has additional columns named <code>mt_doc_type</code> and <code>mt_dotnet_doc_type</code> to select appriopriate rows in the query and to deserialize received JSON into correct type.</p>
<p>Quering the <code>IVehicle</code> type results in SQL which is the same as for types without hierarchies. The differencies are during performing a query for <code>Car</code> or <code>Toyota</code> types.</p>
<pre class="hljs"><code><span class="hljs-keyword">select</span> d.data, d.id, d.mt_doc_type, d.mt_version
<span class="hljs-keyword">from</span> public.mt_doc_ivehicle <span class="hljs-keyword">as</span> d
<span class="hljs-keyword">where</span> d.mt_doc_type = <span class="hljs-string">'toyota'</span> <span class="hljs-keyword">or</span> d.mt_doc_type = <span class="hljs-string">'car'</span>
<span class="hljs-keyword">select</span> d.data, d.id, d.mt_doc_type, d.mt_version
<span class="hljs-keyword">from</span> public.mt_doc_ivehicle <span class="hljs-keyword">as</span> d
<span class="hljs-keyword">where</span> d.mt_doc_type = <span class="hljs-string">'toyota'</span>
</code></pre>
<h4>Summary</h4>
<p>In my personal opinion Marten looks very promising as an alternative to other document databases but cannot be compared directly with them. E.g. RavenDb was created from scratch to build document database when Marten is just a software layer on relational database. Marten gives a foothold for teams which are used to use relational databases. On the other hand, if we would like to see Marten advantages over RavenDb, Marten is fully <a href="https://en.wikipedia.org/wiki/ACID">ACID</a>. RavenDb supports it only in some <a href="https://ayende.com/blog/164066/ravendb-acid-base">queries</a>.</p>
<p>I claim that Marten might be very useful in simple usage scenarios but is not polished enough in more advanced cases, especially if we think about advanced data quering.
Full-text search is doable in Postrgres but it is not supported <a href="https://github.com/JasperFx/marten/issues/39">yet</a>. Queries do not allow to perform <code>GroupBy</code> operation. Multitenancy is going to be made in <code>2.0</code> version but there are still a lot of <a href="https://github.com/JasperFx/marten/issues/435">discussions</a> and uncertainty how to achieve that.
Most of advanced features require database scheme changes. There are <a href="http://jasperfx.github.io/marten/documentation/cli/">tools</a> which enables it but they are not ready to use out of the box. Each project must prepare own adjustments to them. Without good practices, inappriopriate usage may be very harmful especially in production databases.</p>
<p>It must be remembered that Marten is just ORM and has all advantages and drawbacks which ORMs have. Positive aspect is that community concentrated around Marten (including few company contributions) is quite well organised so it forecasts that project will not die and provided functionalities would be still developed and improved.</p>
<p>Examples of Marten usages (as a document database and as en event store) can be found <a href="https://github.com/pblachut/MartenWebApp">here</a>.</p>
</body>
</html>