Database Migrations #22
Replies: 15 comments 29 replies
-
In this forum post I've discussed the db migrations we used at StackOverflow and what we're doing now, which is basically running OrmLite Modify Schema APIs in unit tests which uses Whilst other devs on that thread recommend using DbUp. It's not something we've looked into since I don't see much friction with my preferred solution as-is, whilst everyone has their own solution they prefer & have experience using, resulting in there being not much demand for a OrmLite specific solution which wouldn't justify the time & effort in implementing & maintaining one. |
Beta Was this translation helpful? Give feedback.
-
Its true that there are workarounds and its possible to implement a solution from the primitives OrmLite provides as you yourself said in that forum post, but you ( @mythz ) also said you wanted to implement this feature eventually:
Which is why I'm posting the request here now that this is the new place for the community to engage on things like feature requests. It was the 6th most requested feature on the old UserVoice so its clear that there is at least some demand. Its a request that users have had in the past, and that you have acknowledged that you would like to add eventually, so its worth making sure that the request remains visible, and users continue to have a place to add their support for. 😄 It's also worth mentioning that any users that truly "demand" this feature from a framework are likely simply choosing to use other frameworks because ServiceStack doesn't have any built in Database Migrations, they would then be unlikely to participate anywhere in the community (like the forums, or UserVoice or here) asking for it as a feature and this could be skewing the perspective of how desired the functionality is. As a paid product with a customer acquisition funnel there may be an element of Sampling Bias with respect to users that become engaged enough to ask for new features and the absence of the features they might ask for before they make the decision to engage with the product. |
Beta Was this translation helpful? Give feedback.
-
Yep raising it again here to give it more visibility is a good idea 💡 |
Beta Was this translation helpful? Give feedback.
-
Copy from ServiceStack/ServiceStack@0112c3c#commitcomment-81497037
|
Beta Was this translation helpful? Give feedback.
-
I will attach a minimal implementation (this is based on my usage on a real project) |
Beta Was this translation helpful? Give feedback.
-
I also needed something that i could be run during deploy, independently, so i made a little framework for my own needs, it is a fresh tool so it is going to need more work, but for now is working for me. https://gitlab.com/nukedbit-open/nukedbit-ormlitemigrations https://gitlab.com/nukedbit-open/nukedbit-ormlitemigrations/-/tree/main/src/MigrationTestLib |
Beta Was this translation helpful? Give feedback.
-
Thanks for the feedback! I'll respond to your highlighted issues inline:
There's no time dependent coupling since your App Data Models (which represent the current state of the DB) are not used in the migrations, each migration contains an inner class of the Data Model containing either the initial state (v1) or the deltas between each migration. So your Book example would be written in 2 migrations as: [Description("Initial Book Version")]
public class Migration1000 : MigrationBase
{
public class Book
{
[AutoIncrement]
public int Id { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
public double ToDelete { get; set; }
}
public override void Up()
{
Db.CreateTable<Booking>();
}
}
[Description("Updated Book Table")]
public class Migration1001 : MigrationBase
{
public class Book
{
//[AddColumn] (Optional default)
public string Name { get; set; } //new field
[RemoveColumn]
public double ToDelete { get; set; }
}
public override void Up()
{
Db.Migrate<Booking>();
}
}
This example is just showing the declarative approach for typical schema changes for adding/removing or renaming properties/columns, you can use all of OrmLite (or alt ORMs like Dapper since Db is just an ADO.NET DB Connection). I expect this to be the more productive approach since you can copy new properties from your App's Data Model that you add/remove/rename instead of writing the procedural code to perform the same schema changes. But instead of the declarative approach you can use OrmLite's existing Modify Schema commands to procedurally modify the databse.
You can add a column with
I don't like the magic of automated generation and prefer the explicitness of this approach where the deltas between migrations and what each migration is doing is clearly visible and tracked through source control, it also shouldn't require additional effort as devs would use Migrations to perform the actual schema changes, so it would contain the exact schema changes that are applied to the database and when they're checked-in other devs will be able to apply any new migrations locally so their DB would be in sync with the App's code which are both checked into source control. In future versions we can look at generating the initial migration script to help bootstrap their App to start using this feature, we could generate deltas but if following the process of capturing all schema changes in migrations there would be no need for it.
We record the state of migrations run, but would you also like to also capture the entire SQL executed? Yeah that's a good idea, will add it thx.
The Thanks for the interest & feedback! |
Beta Was this translation helpful? Give feedback.
-
They're only required when using the declarative approach used by |
Beta Was this translation helpful? Give feedback.
-
Yes it's order dependent by name where it executes migrations in alphabetic order and why we suggest a numerical naming strategy like: public class Migration1001 : MigrationBase {}
public class Migration1002 : MigrationBase {} Order is important as it's used to determine which migrations have run / need running. You can use |
Beta Was this translation helpful? Give feedback.
-
Please outline the |
Beta Was this translation helpful? Give feedback.
-
Now that reverting support has been implemented I'll fill in how the rest of it fits together. To demonstrate I'll use the Booking Migrations in OrmLite.Tests/Migrations used in my initial comment to show how revert works. To revert the initial migration of creating the [Notes("Create initial Database")]
public class Migration1000 : MigrationBase
{
public class Booking
{
[AutoIncrement]
public int Id { get; set; }
public string? OldName { get; set; }
public double ToDelete { get; set; }
}
public override void Up() => Db.CreateTable<Booking>();
public override void Down() => Db.DropTable<Booking>();
} To revert the declarative schema changes executed by [Notes("Update Bookings Columns")]
public class Migration1001 : MigrationBase
{
public class Booking
{
[RenameColumn(nameof(Migration1000.Booking.OldName))]
public string? Name { get; set; }
public RoomType RoomType { get; set; }
[RemoveColumn]
public double ToDelete { get; set; }
}
public enum RoomType {} // Values not necessary (Enum's saved as string by default)
public override void Up() => Db.Migrate<Booking>();
public override void Down() => Db.Revert<Booking>();
} Be mindful that whilst this will reverse the schema changes but some operations aren't completely reversible, e.g. if you remove a column then revert to add it again, the data in the column will be lost. Implementing For more complex migrations you'd need to reverse the operations executed in [Notes("Add Player Feature")]
public class Migration1002 : MigrationBase
{
//...
public override void Down()
{
// Clear FK Data
Db.DeleteAll<Level>();
// DROP ForeignKey Tables in dependent order
Db.DropTable<Level>();
Db.DropTable<Player>();
// DROP tables without Foreign Keys in any order
Db.DropTable<Profile>();
Db.DropTable<GameItem>();
}
} Running migrations from command-lineI believe the ef tool uses some magic to scan your App's code to extract out the connection strings and other db context artifacts, IMO that approach is too magical/fragile and a cleaner solution is to just run your App in "task mode" where it has access to your App's DB configuration that's used in executing the registered task, than immediately exits so migrations can be performed by CI in separate stages from your running App. Here's how you'd register the "Migration Tasks" with your App via Modular Startup configuration: public class ConfigureDbMigrations : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureAppHost(afterAppHostInit:appHost => {
var migrator = new Migrator(appHost.Resolve<IDbConnectionFactory>(), typeof(Migration1000).Assembly);
AppTasks.Register("migrate", _ => migrator.Run());
AppTasks.Register("migrate.revert", args => migrator.Revert(args[0]));
AppTasks.Run();
});
} Whilst non-ServiceStack ASP.NET Core Apps can instead run AppTasks before var migrator = new Migrator(app.Services.Resolve<IDbConnectionFactory>(), typeof(MyApp.Migrations.Migration1000).Assembly);
AppTasks.Register("migrate", _ => migrator.Run());
AppTasks.Register("migrate.revert", args => migrator.Revert(args[0]));
AppTasks.Run();
app.Run(); This will allow you to run any pending migrations from the command-line with: $ dotnet run --AppTasks=migrate The format to revert a migration is: $ dotnet run --AppTasks=migrate.revert:<name> Where name can be the name of the Migration you want to revert to (Inclusive) or you can last to revert the last migration or all to revert all migrations. To make this easier to use and remember we'll add them as npm scripts: {
"scripts": {
"migrate": "dotnet run --AppTasks=migrate",
"revert:last": "dotnet run --AppTasks=migrate.revert:last",
"revert:all": "dotnet run --AppTasks=migrate.revert:all"
}
} Which you can run as: $ npm run migrate
$ npm run revert:last
$ npm run revert:all Rider provides a nice UX for running npm scripts which you can do directly from the IDE where it will print all executed SQL output in the console window: Failed migration behaviorExecuting migrations from the command-line would also be how they're run in CI after deploying your App, where if it succeeds the deployment will be successful and the new App will be run, but a failed migration will cause deployment to fail and should revert to using the old App. Migrations are executed within a transaction that's rolled back if it fails, however you should be aware of the limitations of your RDBMS, e.g. newer MySQL versions has caveats on DDL Statements which can be rolled back, but otherwise it's fairly well supported on PostgreSQL, SQL Server, SQLite. Running migrations from unit testsThe migration output should typically be enough to identify any issues, but you can debug in a unit test with: var migrator = new Migrator(DbFactory, typeof(Migration1000).Assembly);
var result = migrator.Run();
result.Succeeded // true if successful
result.MigrationsRun // instances of MIgrationBase, can inspect any exception in `Error` for any Exceptions and `Log` for SQL
result.TypesCompleted // Types succesfully completed You can revert using the var result = migrator.Revert(Migrator.All); Running Migrations individuallyShould you ever need to, you can also run individual Migrations with the static Migrator.Down(DbFactory, new[]{ typeof(Migration1001), typeof(Migration1000) });
Migrator.Up(DbFactory, new[]{ typeof(Migration1000), typeof(Migration1001) }); These lets you execute migration logic out-of-band where they have no impact on the migration table, so they could be useful in developing & iterating migration logic by removing/re-adding a feature without impacting any migration state. and should you need to clear your migration table, e.g. at the start of tests you can run: var db = DbFactory.Open();
Migrator.Clear(db); Feedback WelcomeI believe this feature is functionally complete if you want to help test it it's now available from v6.2.1 that's now available on MyGet. Please also let me know if there's any other features you'd like to see in this first release. |
Beta Was this translation helpful? Give feedback.
-
There is an issue if a specific migration doesn't exist on the branch you're currently working on it will fail, and attempt to re-run all migrations. If someone ran migration1010 on a branch ... and then switched to another branch and ran migration1009 ... it will fail. The migrations runs this SQL command
Which causes it to only be aware of the last migration it ran, however since the last migration1010 doesn't exist on the current branch it seems to think that ALL the migrations need to be run, thus either messing up the database (depending on what the first migration was), or preventing it from running at all since it will fail. I suggest this is changed to fetch all migrations that have been successfully run, and then only run the ones that haven't been run in order as it already does today. |
Beta Was this translation helpful? Give feedback.
-
Another issue that I've ran into, is that if you have a failed migration it will prevent migrations from running unless you go into the database and delete the failed migration row. Shouldn't it just mark it as failed, and attempt to run it again? |
Beta Was this translation helpful? Give feedback.
-
They do, but they also don’t.
When working in a multi-engineer environment, you will often create
unrelated migrations where the order does not matter for a span of
migrations.
In our particular case we just create migrations based on timestamp, this
ensures that the likelihood of name collisions is extremely low. We expect
them to migrate in order, but we also expect that should a migration be
missed, that it will be executed the next time around.
The only reason a migration would be missed, is because it didn’t exist at
the time. Having to rename migrations at release time is undesirable as it
would require having to revert and reapply the migration in the various
environments it was already run against. Additionally in some rare cases
migrations are destructive in nature and therefore reverting them is very
undesirable and usually requires restoring from a backup.
…On Mon, Oct 23, 2023 at 10:04 PM Demis Bellot ***@***.***> wrote:
Migrations absolutely need to be run in sequential order.
—
Reply to this email directly, view it on GitHub
<#22 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AWTY3TH7MTDX3I5GJPSZNY3YA4OZZAVCNFSM5YRIN2O2U5DIOJSWCZC7NNSXTOKENFZWG5LTONUW63SDN5WW2ZLOOQ5TOMZWGQ3TMNQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
The timestamp is generated at the time of writing the feature. When they
get released may not be in the order they were created.
As stated in my previous reply, renaming a migration at the time of release
isn’t desirable at all. Any other framework I’ve worked with doesn’t have a
strict restriction of not running missed migrations.
If you don’t want to default that on, at least add a flag or something.
Running individual migrations creates an extra unnecessary and forgettable
step in the deployment process.
…On Mon, Oct 23, 2023 at 10:24 PM Demis Bellot ***@***.***> wrote:
We're only going to be running and reverting migrations in sequential
order.
I don't see how it's possible migrations are missed if developers adding
timestamps when they commit their completed migration. If for some reason
it was missed you should be able to rename the missed migration to use a
new timestamp.
Otherwise if needed you can also Run Migrations individually
<https://docs.servicestack.net/ormlite/db-migrations#running-migrations-individually>
.
—
Reply to this email directly, view it on GitHub
<#22 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AWTY3TH2ZO4RSOYNBNNVNWLYA4RFNAVCNFSM5YRIN2O2U5DIOJSWCZC7NNSXTOKENFZWG5LTONUW63SDN5WW2ZLOOQ5TOMZWGQ4DOMA>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
TLDR Summary: Can we get first class support for code first Database Migrations... like Django, Laravel, Rails, etc
This feature has been requested before in past support/community channels so I'm re-requesting it here now that ServiceStack has migrated to using GitHub Discussions for community feedback/engagement.
Database Migrations are basically the one headline feature ServiceStack doesn't have a great answer for. It feels, at least to me, like OrmLite's create/drop/alter stable support isn't enough in this day and age. ServiceStack feels extremely modern and compelling, but the moment I have to deal with database schema changes it feels like I'm warping back in time an entire decade or two to when first class code first database change management support was considered an optional extra.
ASP.Net has them from EF/EF.Core, Rails has them, Laravel has them, Django has them, Phoenix has them, lower level database frameworks like Ecto and SqlAlchemy have them... The list goes on and on... to be honest outside of the .Net and Java worlds where there is lots of legacy "enterprise style" "database first" work and the Wordpress ecosystem where everyone is just doing it all manually with SQL even for brand new code... having support for managing the changes you make to your database as you develop your database models, is considered a normal and desirable feature outside of the "micro-framework" world where frameworks like Flask, et.al. make a deliberate design choice come with as few capabilities as possible. (and to be fair often the most popular library that work with a micro-framework will be a wrapper around an ORM library that typically includes migration support)
I've followed ServiceStack for years now and I'm just puzzled at the situation with database migrations. This is a big framework that does a lot of awesome stuff to help my development productivity. Yes its true that no migration framework is perfect for all potential database changes we could theoretically want to make, but neither is any particular ORM perfect at generating the best possible SQL queries, yet ServiceStack has OrmLite and it does an excellent job.
I'm not expecting EF Core style automagically inferred forward backward migrations... I'd be happy with something simpler like integrating FluentMigrator and having something in ServiceStack that automatically generate the migration file that FluentMigrations will use. This would be pretty similar to the workflow in Django which has distinct "make migrations" and "migrate" steps/commands, offers a lot of power and is quite flexible in that it also allows developers to mix their own manually written migrations in with the into the framework's in order to handle more complex things like otherwise unsupported operations and "data migrations" that don't change the table schema.
Beta Was this translation helpful? Give feedback.
All reactions