-
-
Notifications
You must be signed in to change notification settings - Fork 121
Expand file tree
/
Copy pathSqliteDropCreateDatabaseWhenModelChanges.cs
More file actions
199 lines (177 loc) · 8.79 KB
/
Copy pathSqliteDropCreateDatabaseWhenModelChanges.cs
File metadata and controls
199 lines (177 loc) · 8.79 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
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.IO;
using System.Linq;
using SQLite.CodeFirst.Utility;
using System.Diagnostics.CodeAnalysis;
namespace SQLite.CodeFirst
{
/// <summary>
/// An implementation of <see cref="IDatabaseInitializer{TContext}"/> that will always recreate and optionally re-seed the
/// database the first time that a context is used in the app domain or if the model has changed.
/// To seed the database, create a derived class and override the Seed method.
/// <remarks>
/// To detect model changes a new table (implementation of <see cref="IHistory"/>) is added to the database.
/// There is one record in this table which holds the hash of the SQL-statement which was generated from the model
/// executed to create the database. When initializing the database the initializer checks if the hash of the SQL-statement for the
/// model is still the same as the hash in the database. If you use this initializer on a existing database, this initializer
/// will interpret this as model change because of the new <see cref="IHistory"/> table.
/// Notice that a database can be used by more than one context. Therefore the name of the context is saved as a part of the history record.
/// </remarks>
/// </summary>
/// <typeparam name="TContext">The type of the context.</typeparam>
public class SqliteDropCreateDatabaseWhenModelChanges<TContext> : SqliteInitializerBase<TContext>
where TContext : DbContext
{
private readonly Type historyEntityType;
/// <summary>
/// Initializes a new instance of the <see cref="SqliteDropCreateDatabaseWhenModelChanges{TContext}"/> class.
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
public SqliteDropCreateDatabaseWhenModelChanges(DbModelBuilder modelBuilder)
: this(modelBuilder, typeof(History), null)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="SqliteDropCreateDatabaseWhenModelChanges{TContext}"/> class.
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="defaultCollation">The default collation applied to all string columns. Explicit <see cref="CollateAttribute"/>s take precedence.</param>
public SqliteDropCreateDatabaseWhenModelChanges(DbModelBuilder modelBuilder, Collation defaultCollation)
: this(modelBuilder, typeof(History), defaultCollation)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="SqliteDropCreateDatabaseWhenModelChanges{TContext}"/> class.
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="historyEntityType">Type of the history entity (must implement <see cref="IHistory"/> and provide an parameterless constructor).</param>
public SqliteDropCreateDatabaseWhenModelChanges(DbModelBuilder modelBuilder, Type historyEntityType)
: this(modelBuilder, historyEntityType, null)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="SqliteDropCreateDatabaseWhenModelChanges{TContext}"/> class.
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="historyEntityType">Type of the history entity (must implement <see cref="IHistory"/> and provide an parameterless constructor).</param>
/// <param name="defaultCollation">The default collation applied to all string columns. Explicit <see cref="CollateAttribute"/>s take precedence.</param>
public SqliteDropCreateDatabaseWhenModelChanges(DbModelBuilder modelBuilder, Type historyEntityType, Collation defaultCollation)
: base(modelBuilder, defaultCollation)
{
this.historyEntityType = historyEntityType;
ConfigureHistoryEntity();
}
protected void ConfigureHistoryEntity()
{
HistoryEntityTypeValidator.EnsureValidType(historyEntityType);
ModelBuilder.RegisterEntityType(historyEntityType);
}
/// <summary>
/// Initialize the database for the given context.
/// Generates the SQLite-DDL from the model and executes it against the database.
/// After that the <see cref="Seed" /> method is executed.
/// All actions are be executed in transactions.
/// </summary>
/// <param name="context">The context.</param>
public override void InitializeDatabase(TContext context)
{
string databaseFilePath = GetDatabasePathFromContext(context);
bool dbExists = InMemoryAwareFile.Exists(databaseFilePath);
if (dbExists)
{
if (IsSameModel(context))
{
return;
}
FileAttributes? attributes = InMemoryAwareFile.GetFileAttributes(databaseFilePath);
CloseDatabase(context);
DeleteDatabase(context, databaseFilePath);
base.InitializeDatabase(context);
InMemoryAwareFile.SetFileAttributes(databaseFilePath, attributes);
SaveHistory(context);
}
else
{
base.InitializeDatabase(context);
SaveHistory(context);
}
}
/// <summary>
/// Called to drop/remove Database file from disk.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="databaseFilePath">Filename of Database to be removed.</param>
protected virtual void DeleteDatabase(TContext context, string databaseFilePath)
{
InMemoryAwareFile.Delete(databaseFilePath);
}
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect", Justification = "Required.")]
private static void CloseDatabase(TContext context)
{
context.Database.Connection.Close();
GC.Collect();
GC.WaitForPendingFinalizers();
}
private void SaveHistory(TContext context)
{
var hash = GetHashFromModel(context.Database.Connection);
var history = GetHistoryRecord(context);
EntityState entityState;
if (history == null)
{
history = (IHistory)Activator.CreateInstance(historyEntityType);
entityState = EntityState.Added;
}
else
{
entityState = EntityState.Modified;
}
history.Context = context.GetType().FullName;
history.Hash = hash;
history.CreateDate = DateTime.UtcNow;
context.Set(historyEntityType).Attach(history);
context.Entry(history).State = entityState;
context.SaveChanges();
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
private bool IsSameModel(TContext context)
{
var hash = GetHashFromModel(context.Database.Connection);
try
{
var history = GetHistoryRecord(context);
return history?.Hash == hash;
}
catch (Exception)
{
// This happens if the history table does not exist.
// So it covers also the case with a null byte file (see SqliteCreateDatabaseIfNotExists).
return false;
}
}
private IHistory GetHistoryRecord(TContext context)
{
// Yes, it seams to be complicated but it has to be done this way
// in order to be supported by .NET 4.0.
DbQuery dbQuery = context.Set(historyEntityType).AsNoTracking();
IEnumerable<IHistory> records = Enumerable.Cast<IHistory>(dbQuery);
// A database can be shared by several contexts, so the record must be looked up by the
// context key that SaveHistory writes. An unscoped lookup would match every context's
// record and throw on a shared history table.
return HistoryRecordSelector.SelectForContext(records, context.GetType().FullName);
}
private string GetHashFromModel(DbConnection connection)
{
var sql = GetSqlFromModel(connection);
string hash = HashCreator.CreateHash(sql);
return hash;
}
private string GetSqlFromModel(DbConnection connection)
{
var model = ModelBuilder.Build(connection);
var sqliteSqlGenerator = new SqliteSqlGenerator(DefaultCollation);
return sqliteSqlGenerator.Generate(model.StoreModel);
}
}
}