mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-14 19:04:51 +08:00
Data migration management class and automatic discovery mecanism
--HG-- branch : dev
This commit is contained in:
294
src/Orchard.Tests/DataMigration/DataMigrationTests.cs
Normal file
294
src/Orchard.Tests/DataMigration/DataMigrationTests.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Autofac;
|
||||
using NHibernate;
|
||||
using NUnit.Framework;
|
||||
using Orchard.ContentManagement.Records;
|
||||
using Orchard.Data;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Environment.Extensions.Folders;
|
||||
using Orchard.Environment.Extensions.Loaders;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.Tests.ContentManagement;
|
||||
using Orchard.DataMigration;
|
||||
|
||||
namespace Orchard.Tests.DataMigration {
|
||||
[TestFixture]
|
||||
public class DataMigrationTests {
|
||||
private IContainer _container;
|
||||
private IExtensionManager _manager;
|
||||
private StubFolders _folders;
|
||||
private IDataMigrationManager _dataMigrationManager;
|
||||
private IRepository<DataMigrationRecord> _repository;
|
||||
|
||||
private ISessionFactory _sessionFactory;
|
||||
private ISession _session;
|
||||
|
||||
[SetUp]
|
||||
public void Init() {
|
||||
Init(Enumerable.Empty<Type>());
|
||||
}
|
||||
|
||||
public void Init(IEnumerable<Type> dataMigrations) {
|
||||
|
||||
var databaseFileName = System.IO.Path.GetTempFileName();
|
||||
_sessionFactory = DataUtility.CreateSessionFactory(
|
||||
databaseFileName,
|
||||
typeof(DataMigrationRecord),
|
||||
typeof(ContentItemVersionRecord),
|
||||
typeof(ContentItemRecord),
|
||||
typeof(ContentTypeRecord));
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
_folders = new StubFolders();
|
||||
builder.RegisterInstance(_folders).As<IExtensionFolders>();
|
||||
builder.RegisterType<ExtensionManager>().As<IExtensionManager>();
|
||||
builder.RegisterType<DataMigrationManager>().As<IDataMigrationManager>();
|
||||
builder.RegisterType<DefaultDataMigrationGenerator>().As<IDataMigrationGenerator>();
|
||||
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>));
|
||||
_session = _sessionFactory.OpenSession();
|
||||
builder.RegisterInstance(new DefaultContentManagerTests.TestSessionLocator(_session)).As<ISessionLocator>();
|
||||
foreach(var type in dataMigrations) {
|
||||
builder.RegisterType(type).As<IDataMigration>();
|
||||
}
|
||||
_container = builder.Build();
|
||||
_manager = _container.Resolve<IExtensionManager>();
|
||||
_dataMigrationManager = _container.Resolve<IDataMigrationManager>();
|
||||
_repository = _container.Resolve<IRepository<DataMigrationRecord>>();
|
||||
|
||||
}
|
||||
|
||||
public class StubFolders : IExtensionFolders {
|
||||
public StubFolders() {
|
||||
Manifests = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Manifests { get; set; }
|
||||
|
||||
public IEnumerable<ExtensionDescriptor> AvailableExtensions() {
|
||||
foreach (var e in Manifests) {
|
||||
string name = e.Key;
|
||||
var parseResult = ExtensionFolders.ParseManifest(Manifests[name]);
|
||||
yield return ExtensionFolders.GetDescriptorForExtension("~/", name, "Module", parseResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DataMigrationEmpty : IDataMigration {
|
||||
public string Feature {
|
||||
get { return "Feature1"; }
|
||||
}
|
||||
}
|
||||
|
||||
public class DataMigration11 : IDataMigration {
|
||||
public string Feature {
|
||||
get { return "Feature1"; }
|
||||
}
|
||||
}
|
||||
|
||||
public class DataMigration11Create : IDataMigration {
|
||||
public string Feature {
|
||||
get { return "Feature1"; }
|
||||
}
|
||||
|
||||
public int Create() {
|
||||
return 999;
|
||||
}
|
||||
}
|
||||
|
||||
public class DataMigrationCreateCanBeFollowedByUpdates : IDataMigration {
|
||||
public string Feature {
|
||||
get { return "Feature1"; }
|
||||
}
|
||||
|
||||
public int Create() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
public int UpdateFrom42() {
|
||||
return 666;
|
||||
}
|
||||
}
|
||||
|
||||
public class DataMigrationSameMigrationClassCanEvolve : IDataMigration {
|
||||
public string Feature {
|
||||
get { return "Feature1"; }
|
||||
}
|
||||
|
||||
public int Create() {
|
||||
return 999;
|
||||
}
|
||||
|
||||
public int UpdateFrom42() {
|
||||
return 666;
|
||||
}
|
||||
|
||||
public int UpdateFrom666() {
|
||||
return 999;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class DataMigrationDependenciesModule1 : IDataMigration {
|
||||
public string Feature {
|
||||
get { return "Feature1"; }
|
||||
}
|
||||
|
||||
public int Create() {
|
||||
return 999;
|
||||
}
|
||||
}
|
||||
public class DataMigrationDependenciesModule2 : IDataMigration {
|
||||
public string Feature {
|
||||
get { return "Feature2"; }
|
||||
}
|
||||
|
||||
public int Create() {
|
||||
return 999;
|
||||
}
|
||||
}
|
||||
|
||||
public class StubLoaders : ExtensionLoaderBase {
|
||||
#region Implementation of IExtensionLoader
|
||||
|
||||
public override int Order {
|
||||
get { return 1; }
|
||||
}
|
||||
|
||||
public override ExtensionProbeEntry Probe(ExtensionDescriptor descriptor) {
|
||||
return new ExtensionProbeEntry { Descriptor = descriptor, Loader = this };
|
||||
}
|
||||
|
||||
public override ExtensionEntry Load(ExtensionDescriptor descriptor) {
|
||||
return new ExtensionEntry { Descriptor = descriptor, ExportedTypes = new Type[0] };
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DataMigrationShouldDoNothingIfNoDataMigrationIsProvidedForFeature() {
|
||||
Init(new Type[] {typeof (DataMigrationEmpty)});
|
||||
|
||||
_folders.Manifests.Add("Module2", @"
|
||||
name: Module2
|
||||
version: 0.1
|
||||
orchardversion: 1
|
||||
features:
|
||||
Feature1:
|
||||
Description: Feature
|
||||
");
|
||||
|
||||
_dataMigrationManager.Upgrade("Feature1");
|
||||
Assert.That(_repository.Table.Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DataMigrationShouldDoNothingIfNoUpgradeOrCreateMethodWasFound() {
|
||||
Init(new Type[] { typeof(DataMigration11) });
|
||||
|
||||
_folders.Manifests.Add("Module1", @"
|
||||
name: Module1
|
||||
version: 0.1
|
||||
orchardversion: 1
|
||||
features:
|
||||
Feature1:
|
||||
Description: Feature
|
||||
");
|
||||
|
||||
_dataMigrationManager.Upgrade("Feature1");
|
||||
Assert.That(_repository.Table.Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateShouldReturnVersionNumber() {
|
||||
Init(new Type[] { typeof(DataMigration11Create) });
|
||||
|
||||
_folders.Manifests.Add("Module1", @"
|
||||
name: Module1
|
||||
version: 0.1
|
||||
orchardversion: 1
|
||||
features:
|
||||
Feature1:
|
||||
Description: Feature
|
||||
");
|
||||
|
||||
_dataMigrationManager.Upgrade("Feature1");
|
||||
Assert.That(_repository.Table.Count(), Is.EqualTo(1));
|
||||
Assert.That(_repository.Table.First().Current, Is.EqualTo(999));
|
||||
Assert.That(_repository.Table.First().DataMigrationClass, Is.EqualTo("Orchard.Tests.DataMigration.DataMigrationTests+DataMigration11Create"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateCanBeFollowedByUpdates() {
|
||||
Init(new Type[] {typeof (DataMigrationCreateCanBeFollowedByUpdates)});
|
||||
|
||||
_folders.Manifests.Add("Module1", @"
|
||||
name: Module1
|
||||
version: 0.1
|
||||
orchardversion: 1
|
||||
features:
|
||||
Feature1:
|
||||
Description: Feature
|
||||
");
|
||||
|
||||
_dataMigrationManager.Upgrade("Feature1");
|
||||
Assert.That(_repository.Table.Count(), Is.EqualTo(1));
|
||||
Assert.That(_repository.Table.First().Current, Is.EqualTo(666));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SameMigrationClassCanEvolve() {
|
||||
Init(new Type[] { typeof(DataMigrationSameMigrationClassCanEvolve) });
|
||||
|
||||
_folders.Manifests.Add("Module1", @"
|
||||
name: Module1
|
||||
version: 0.1
|
||||
orchardversion: 1
|
||||
features:
|
||||
Feature1:
|
||||
Description: Feature
|
||||
");
|
||||
_repository.Create(new DataMigrationRecord() {
|
||||
Current = 42,
|
||||
DataMigrationClass = "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationSameMigrationClassCanEvolve"
|
||||
});
|
||||
|
||||
_dataMigrationManager.Upgrade("Feature1");
|
||||
Assert.That(_repository.Table.Count(), Is.EqualTo(1));
|
||||
Assert.That(_repository.Table.First().Current, Is.EqualTo(999));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DependenciesShouldBeUpgradedFirst() {
|
||||
|
||||
Init(new Type[] { typeof(DataMigrationDependenciesModule1), typeof(DataMigrationDependenciesModule2) });
|
||||
|
||||
_folders.Manifests.Add("Module1", @"
|
||||
name: Module1
|
||||
version: 0.1
|
||||
orchardversion: 1
|
||||
features:
|
||||
Feature1:
|
||||
Description: Feature
|
||||
Dependencies: Feature2
|
||||
");
|
||||
|
||||
_folders.Manifests.Add("Module2", @"
|
||||
name: Module2
|
||||
version: 0.1
|
||||
orchardversion: 1
|
||||
features:
|
||||
Feature2:
|
||||
Description: Feature
|
||||
");
|
||||
_dataMigrationManager.Upgrade("Feature1");
|
||||
Assert.That(_repository.Table.Count(), Is.EqualTo(2));
|
||||
Assert.That(_repository.Fetch(d => d.Current == 999).Count(), Is.EqualTo(2));
|
||||
Assert.That(_repository.Fetch(d => d.DataMigrationClass == "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationDependenciesModule1").Count(), Is.EqualTo(1));
|
||||
Assert.That(_repository.Fetch(d => d.DataMigrationClass == "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationDependenciesModule2").Count(), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -180,6 +180,7 @@
|
||||
<Compile Include="ContentManagement\Records\GammaRecord.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="DataMigration\DataMigrationTests.cs" />
|
||||
<Compile Include="DataUtility.cs" />
|
||||
<Compile Include="Data\Builders\SessionFactoryBuilderTests.cs" />
|
||||
<Compile Include="Data\RepositoryTests.cs" />
|
||||
|
@@ -0,0 +1,4 @@
|
||||
namespace Orchard.DataMigration.Commands {
|
||||
public interface IDataMigrationCommand {
|
||||
}
|
||||
}
|
5
src/Orchard/DataMigration/DataMigration.cs
Normal file
5
src/Orchard/DataMigration/DataMigration.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Orchard.DataMigration {
|
||||
public abstract class DataMigration : IDataMigration {
|
||||
public abstract string Feature { get; }
|
||||
}
|
||||
}
|
124
src/Orchard/DataMigration/DataMigrationManager.cs
Normal file
124
src/Orchard/DataMigration/DataMigrationManager.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Orchard.Data;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.Logging;
|
||||
|
||||
namespace Orchard.DataMigration {
|
||||
/// <summary>
|
||||
/// Reponsible for maintaining the knowledge of data migration in a per tenant table
|
||||
/// </summary>
|
||||
public class DataMigrationManager : IDataMigrationManager {
|
||||
private readonly IEnumerable<IDataMigration> _dataMigrations;
|
||||
private readonly IRepository<DataMigrationRecord> _dataMigrationRepository;
|
||||
private readonly IDataMigrationGenerator _dataMigrationGenerator;
|
||||
private readonly IExtensionManager _extensionManager;
|
||||
|
||||
public DataMigrationManager(
|
||||
IEnumerable<IDataMigration> dataMigrations,
|
||||
IRepository<DataMigrationRecord> dataMigrationRepository,
|
||||
IDataMigrationGenerator dataMigrationGenerator,
|
||||
IExtensionManager extensionManager) {
|
||||
_dataMigrations = dataMigrations;
|
||||
_dataMigrationRepository = dataMigrationRepository;
|
||||
_dataMigrationGenerator = dataMigrationGenerator;
|
||||
_extensionManager = extensionManager;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void Upgrade(string feature) {
|
||||
|
||||
// proceed with dependent features first, whatever the module it's in
|
||||
var dependencies = _extensionManager
|
||||
.AvailableExtensions()
|
||||
.SelectMany(e => e.Features)
|
||||
.Where(f => String.Equals(f.Name, feature, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => f.Dependencies != null)
|
||||
.SelectMany( f => f.Dependencies )
|
||||
.ToList();
|
||||
|
||||
foreach(var dependency in dependencies) {
|
||||
Upgrade(dependency);
|
||||
}
|
||||
|
||||
var migrations = GetDataMigrations(feature);
|
||||
|
||||
// apply update methods to each migration class for the module
|
||||
foreach ( var migration in migrations ) {
|
||||
// copy the objet for the Linq query
|
||||
var tempMigration = migration;
|
||||
|
||||
// get current version for this migration
|
||||
var dataMigrationRecord = _dataMigrationRepository.Table
|
||||
.Where(dm => dm.DataMigrationClass == tempMigration.GetType().FullName)
|
||||
.FirstOrDefault();
|
||||
|
||||
var updateMethodNameRegex = new Regex(@"^UpdateFrom(?<version>\d+)$", RegexOptions.Compiled);
|
||||
var current = 0;
|
||||
if(dataMigrationRecord != null) {
|
||||
current = dataMigrationRecord.Current;
|
||||
}
|
||||
|
||||
// do we need to call Create() ?
|
||||
if(current == 0) {
|
||||
// try to resolve a Create method
|
||||
|
||||
var createMethod = migration.GetType().GetMethod("Create", BindingFlags.Public | BindingFlags.Instance);
|
||||
if(createMethod != null && createMethod.ReturnType == typeof(int)) {
|
||||
current = (int)createMethod.Invoke(migration, new object[0]);
|
||||
}
|
||||
else {
|
||||
var commands = _dataMigrationGenerator.CreateCommands();
|
||||
/// TODO: Execute commands and define current version number
|
||||
}
|
||||
}
|
||||
|
||||
// update methods might also be called after Create()
|
||||
var lookupTable = new Dictionary<int, MethodInfo>();
|
||||
// construct a lookup table with all managed initial versions
|
||||
foreach(var methodInfo in migration.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) {
|
||||
var match = updateMethodNameRegex.Match(methodInfo.Name);
|
||||
if(match.Success) {
|
||||
lookupTable.Add(int.Parse(match.Groups["version"].Value), methodInfo);
|
||||
}
|
||||
}
|
||||
|
||||
while(lookupTable.ContainsKey(current)) {
|
||||
try {
|
||||
Logger.Information("Applying migration for {0} from version {1}", feature, current);
|
||||
current = (int)lookupTable[current].Invoke(migration, new object[0]);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Error(ex, "An unexpected error orccured while applying migration on {0} from version {1}", feature, current);
|
||||
}
|
||||
}
|
||||
|
||||
// if current is 0, it means no upgrade/create method was found or succeeded
|
||||
if ( current != 0 ) {
|
||||
if (dataMigrationRecord == null) {
|
||||
_dataMigrationRepository.Create(new DataMigrationRecord {Current = current, DataMigrationClass = migration.GetType().FullName});
|
||||
}
|
||||
else {
|
||||
dataMigrationRecord.Current = current;
|
||||
_dataMigrationRepository.Update(dataMigrationRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the available IDataMigration instances for a specific module
|
||||
/// </summary>
|
||||
public IEnumerable<IDataMigration> GetDataMigrations(string feature) {
|
||||
return _dataMigrations
|
||||
.Where(dm => String.Equals(dm.Feature, feature, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
7
src/Orchard/DataMigration/DataMigrationRecord.cs
Normal file
7
src/Orchard/DataMigration/DataMigrationRecord.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Orchard.DataMigration {
|
||||
public class DataMigrationRecord {
|
||||
public virtual int Id { get; set; }
|
||||
public virtual string DataMigrationClass { get; set; }
|
||||
public virtual int Current { get; set; }
|
||||
}
|
||||
}
|
11
src/Orchard/DataMigration/DefaultDataMigrationGenerator.cs
Normal file
11
src/Orchard/DataMigration/DefaultDataMigrationGenerator.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.DataMigration.Commands;
|
||||
|
||||
namespace Orchard.DataMigration {
|
||||
public class DefaultDataMigrationGenerator : IDataMigrationGenerator {
|
||||
public IEnumerable<IDataMigrationCommand> CreateCommands() {
|
||||
return Enumerable.Empty<IDataMigrationCommand>();
|
||||
}
|
||||
}
|
||||
}
|
5
src/Orchard/DataMigration/IDataMigration.cs
Normal file
5
src/Orchard/DataMigration/IDataMigration.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Orchard.DataMigration {
|
||||
public interface IDataMigration : IDependency {
|
||||
string Feature { get; }
|
||||
}
|
||||
}
|
9
src/Orchard/DataMigration/IDataMigrationGenerator.cs
Normal file
9
src/Orchard/DataMigration/IDataMigrationGenerator.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.DataMigration.Commands;
|
||||
|
||||
namespace Orchard.DataMigration {
|
||||
// Builds and runs the representative migration create calls
|
||||
public interface IDataMigrationGenerator : IDependency {
|
||||
IEnumerable<IDataMigrationCommand> CreateCommands();
|
||||
}
|
||||
}
|
5
src/Orchard/DataMigration/IDataMigrationManager.cs
Normal file
5
src/Orchard/DataMigration/IDataMigrationManager.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Orchard.DataMigration {
|
||||
public interface IDataMigrationManager : IDependency {
|
||||
void Upgrade(string feature);
|
||||
}
|
||||
}
|
@@ -348,6 +348,14 @@
|
||||
<Compile Include="ContentManagement\ViewModels\TemplateViewModel.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="DataMigration\Commands\IDataMigrationCommand.cs" />
|
||||
<Compile Include="DataMigration\DefaultDataMigrationGenerator.cs" />
|
||||
<Compile Include="DataMigration\IDataMigrationGenerator.cs" />
|
||||
<Compile Include="DataMigration\DataMigration.cs" />
|
||||
<Compile Include="DataMigration\DataMigrationManager.cs" />
|
||||
<Compile Include="DataMigration\DataMigrationRecord.cs" />
|
||||
<Compile Include="DataMigration\IDataMigration.cs" />
|
||||
<Compile Include="DataMigration\IDataMigrationManager.cs" />
|
||||
<Compile Include="Data\SessionLocator.cs" />
|
||||
<Compile Include="Data\IRepository.cs" />
|
||||
<Compile Include="Data\ISessionLocator.cs" />
|
||||
|
Reference in New Issue
Block a user