1
1
using System ;
2
+ using System . Collections . Generic ;
3
+ using System . Data ;
2
4
using System . Linq ;
3
5
using Orchard . ContentManagement . MetaData ;
4
6
using Orchard . Core . Common . Models ;
5
7
using Orchard . Core . Contents . Extensions ;
6
8
using Orchard . Data ;
7
9
using Orchard . Data . Migration ;
10
+ using Orchard . Environment . Configuration ;
8
11
9
12
namespace Orchard . Core . Common {
10
13
public class Migrations : DataMigrationImpl {
11
14
private readonly IRepository < IdentityPartRecord > _identityPartRepository ;
15
+ private readonly ISessionFactoryHolder _sessionFactoryHolder ;
16
+ private readonly ShellSettings _shellSettings ;
12
17
18
+ private HashSet < string > _existingIndexNames = new HashSet < string > ( ) ;
13
19
14
- public Migrations ( IRepository < IdentityPartRecord > identityPartRepository ) {
20
+
21
+ public Migrations (
22
+ IRepository < IdentityPartRecord > identityPartRepository ,
23
+ ISessionFactoryHolder sessionFactoryHolder ,
24
+ ShellSettings shellSettings ) {
15
25
_identityPartRepository = identityPartRepository ;
26
+ _sessionFactoryHolder = sessionFactoryHolder ;
27
+ _shellSettings = shellSettings ;
16
28
}
17
29
18
30
@@ -155,35 +167,72 @@ public int UpdateFrom5() {
155
167
return 6 ;
156
168
}
157
169
170
+ // When upgrading from version 6 of 1.10.x (up until version 9), we'll just execute the same steps, but in a
171
+ // different order.
158
172
public int UpdateFrom6 ( ) {
159
- SchemaBuilder . AlterTable ( nameof ( IdentityPartRecord ) , table => table
160
- . CreateIndex ( $ "IDX_ { nameof ( IdentityPartRecord ) } _ { nameof ( IdentityPartRecord . Identifier ) } " , nameof ( IdentityPartRecord . Identifier ) ) ) ;
173
+ // This is the original step of the dev branch.
174
+ AddIndexForIdentityPartRecordIdentifier ( ) ;
161
175
162
176
return 7 ;
163
177
}
164
178
165
179
public int UpdateFrom7 ( ) {
166
- // The Container_Id is basically a foreign key, used in several queries
167
- SchemaBuilder . AlterTable ( nameof ( CommonPartRecord ) , table => {
168
- table . CreateIndex ( $ "IDX_{ nameof ( CommonPartRecord ) } _Container_id",
169
- "Container_id" ) ;
170
- } ) ;
180
+ // This is the original step of the dev branch.
181
+ AddIndexForCommonPartRecordContainerId ( ) ;
182
+
183
+ // When upgrading from version 7 of 1.10.x, this index isn't created yet, so we need to run this step
184
+ // "again". On the other hand, AddIndexesForCommonPartOwner in UpdateFrom8 won't do anything, because those
185
+ // indexes were added in the 1.10.x version of UpdateFrom6.
186
+ AddIndexForIdentityPartRecordIdentifier ( ) ;
171
187
172
188
return 8 ;
173
189
}
174
190
175
191
public int UpdateFrom8 ( ) {
176
- // Studying SQL Server query execution plans we noticed that when the system
177
- // tries to find content items for requests such as
178
- // "The items of type TTT owned by me, ordered from the most recent"
179
- // the existing indexes are not used. SQL Server does an index scan on the
180
- // Primary key for CommonPartRecord. This may lead to annoying deadlocks when
181
- // there are two concurrent transactions that are doing both this kind of query
182
- // as well as an update (or insert) in the CommonPartRecord.
183
- // Tests show that this can be easily fixed by adding a non-clustered index
184
- // with these keys: OwnerId, {one of PublishedUTC, ModifiedUTC, CreatedUTC}.
185
- // That means we need three indexes (one for each DateTime) to support ordering
186
- // on either of them.
192
+ // This is the original step of the dev branch.
193
+ AddIndexesForCommonPartOwner ( ) ;
194
+
195
+ // When upgrading from version 8 of 1.10.x, this index isn't created yet, so we need to run this step
196
+ // "again"
197
+ AddIndexForCommonPartRecordContainerId ( ) ;
198
+
199
+ return 9 ;
200
+ }
201
+
202
+ // This change was originally UpdateFrom7 on 1.10.x and UpdateFrom6 on dev.
203
+ private void AddIndexForIdentityPartRecordIdentifier ( ) {
204
+ var indexName = $ "IDX_{ nameof ( IdentityPartRecord ) } _{ nameof ( IdentityPartRecord . Identifier ) } ";
205
+
206
+ if ( IndexExists ( nameof ( IdentityPartRecord ) , indexName ) ) return ;
207
+
208
+ SchemaBuilder . AlterTable ( nameof ( IdentityPartRecord ) , table => table . CreateIndex (
209
+ indexName ,
210
+ nameof ( IdentityPartRecord . Identifier ) ) ) ;
211
+
212
+ IndexCreated ( nameof ( IdentityPartRecord ) , indexName ) ;
213
+ }
214
+
215
+ // This change was originally UpdateFrom8 on 1.10.x and UpdateFrom7 on dev.
216
+ private void AddIndexForCommonPartRecordContainerId ( ) {
217
+ var indexName = $ "IDX_{ nameof ( CommonPartRecord ) } _Container_id";
218
+
219
+ if ( IndexExists ( nameof ( CommonPartRecord ) , indexName ) ) return ;
220
+
221
+ // Container_Id is used in several queries like a foreign key.
222
+ SchemaBuilder . AlterTable ( nameof ( CommonPartRecord ) , table => table . CreateIndex ( indexName , "Container_id" ) ) ;
223
+
224
+ IndexCreated ( nameof ( CommonPartRecord ) , indexName ) ;
225
+ }
226
+
227
+ // This change was originally UpdateFrom6 on 1.10.x and UpdateFrom8 on dev.
228
+ private void AddIndexesForCommonPartOwner ( ) {
229
+ // Studying SQL Server query execution plans we noticed that when the system tries to find content items for
230
+ // requests such as "The items of type TTT owned by me, ordered from the most recent" the existing indexes
231
+ // are not used. SQL Server does an index scan on the Primary key for CommonPartRecord. This may lead to
232
+ // annoying deadlocks when there are two concurrent transactions that are doing both this kind of query as
233
+ // well as an update (or insert) in the CommonPartRecord. Tests show that this can be easily fixed by adding
234
+ // a non-clustered index with these keys: OwnerId, {one of PublishedUTC, ModifiedUTC, CreatedUTC}. That
235
+ // means we need three indexes (one for each DateTime) to support ordering on either of them.
187
236
188
237
// The queries we analyzed look like (in pseudo sql)
189
238
// SELECT TOP (N) *
@@ -198,20 +247,51 @@ public int UpdateFrom8() {
198
247
// and this_.Published = 1
199
248
// ORDER BY
200
249
// commonpart2_PublishedUtc desc
250
+ var createdUtcIndexName = $ "IDX_{ nameof ( CommonPartRecord ) } _OwnedBy_ByCreation";
251
+ var modifiedUtcIndexName = $ "IDX_{ nameof ( CommonPartRecord ) } _OwnedBy_ByModification";
252
+ var publishedUtcIndexName = $ "IDX_{ nameof ( CommonPartRecord ) } _OwnedBy_ByPublication";
253
+
254
+ if ( IndexExists ( nameof ( CommonPartRecord ) , createdUtcIndexName ) ) return ;
201
255
202
256
SchemaBuilder . AlterTable ( nameof ( CommonPartRecord ) , table => {
203
- table . CreateIndex ( $ "IDX_{ nameof ( CommonPartRecord ) } _OwnedBy_ByCreation",
204
- nameof ( CommonPartRecord . OwnerId ) ,
205
- nameof ( CommonPartRecord . CreatedUtc ) ) ;
206
- table . CreateIndex ( $ "IDX_{ nameof ( CommonPartRecord ) } _OwnedBy_ByModification",
207
- nameof ( CommonPartRecord . OwnerId ) ,
208
- nameof ( CommonPartRecord . ModifiedUtc ) ) ;
209
- table . CreateIndex ( $ "IDX_{ nameof ( CommonPartRecord ) } _OwnedBy_ByPublication",
210
- nameof ( CommonPartRecord . OwnerId ) ,
211
- nameof ( CommonPartRecord . PublishedUtc ) ) ;
257
+ table . CreateIndex ( createdUtcIndexName , nameof ( CommonPartRecord . OwnerId ) , nameof ( CommonPartRecord . CreatedUtc ) ) ;
258
+ table . CreateIndex ( modifiedUtcIndexName , nameof ( CommonPartRecord . OwnerId ) , nameof ( CommonPartRecord . ModifiedUtc ) ) ;
259
+ table . CreateIndex ( publishedUtcIndexName , nameof ( CommonPartRecord . OwnerId ) , nameof ( CommonPartRecord . PublishedUtc ) ) ;
212
260
} ) ;
213
261
214
- return 9 ;
262
+ IndexCreated ( nameof ( CommonPartRecord ) , createdUtcIndexName ) ;
263
+ IndexCreated ( nameof ( CommonPartRecord ) , modifiedUtcIndexName ) ;
264
+ IndexCreated ( nameof ( CommonPartRecord ) , publishedUtcIndexName ) ;
265
+ }
266
+
267
+ private bool IndexExists ( string tableName , string indexName ) {
268
+ var tenantTablesPrefix = string . IsNullOrEmpty ( _shellSettings . DataTablePrefix )
269
+ ? string . Empty : $ "{ _shellSettings . DataTablePrefix } _";
270
+
271
+ if ( ! _existingIndexNames . Any ( ) ) {
272
+ // Database-agnostic way of checking the existence of an index.
273
+ using ( var session = _sessionFactoryHolder . GetSessionFactory ( ) . OpenSession ( ) ) {
274
+ var connection = session . Connection ?? throw new InvalidOperationException (
275
+ "The database connection object should derive from DbConnection to check if an index exists." ) ;
276
+
277
+ var indexes = connection . GetSchema ( "Indexes" ) . Rows . Cast < DataRow > ( ) ;
278
+
279
+ if ( ! string . IsNullOrEmpty ( tenantTablesPrefix ) ) {
280
+ indexes = indexes . Where ( row => row [ "TABLE_NAME" ] . ToString ( ) . StartsWith ( tenantTablesPrefix ) ) ;
281
+ }
282
+
283
+ _existingIndexNames = indexes . Select ( row => $ "{ row [ "TABLE_NAME" ] } .{ row [ "INDEX_NAME" ] } ") . ToHashSet ( ) ;
284
+ }
285
+ }
286
+
287
+ return _existingIndexNames . Contains ( $ "{ SchemaBuilder . TableDbName ( tableName ) } .{ tenantTablesPrefix } { indexName } ") ;
288
+ }
289
+
290
+ private void IndexCreated ( string tableName , string indexName ) {
291
+ var tenantTablesPrefix = string . IsNullOrEmpty ( _shellSettings . DataTablePrefix )
292
+ ? string . Empty : $ "{ _shellSettings . DataTablePrefix } _";
293
+
294
+ _existingIndexNames . Add ( $ "{ SchemaBuilder . TableDbName ( tableName ) } .{ tenantTablesPrefix } { indexName } ") ;
215
295
}
216
296
}
217
297
}
0 commit comments