Wednesday, October 03, 2007 -
Just last night I ran my first "Green Light" unit tests on what I think is going to be a pretty groovy new feature: Migrations. Usually I don't like to talk much about functionality until it's 90%-ish, but I figured that it might be good to let everyone know the what/how/why now so I can take all your lovely comments and see if I can pop them in before it gets released.
For those who don't know, Migrations allow you to build out your DB schema using code. It's a Rails Thing, and if you're not familiar with them you can watch one of my webcasts where I use them (right around the 3 minute mark), or just read up on them here.
The concept is that you version the building of your DB into code files, called "Migrations". Using code, you can build and change your DB as needed, and keep a reference of this in your app's codebase. Since you're using code to build out the schema, you can do all kinds of neat things like add data, ping services, send emails, etc whenever you change around the schema. This puts the DB work into your app and out of the designer, which in some cases can be neat.
Code Sample
Currently, all migrations are built off of an abstract base class called, as you might guess, SubSonic.Migration. Each migration must inherit from this base class, and must also implement two methods: Up (versions your DB upward) and Down(versions it down). Here's an example:
public class Migration001:Migration { public override void Up() { TableSchema.TableCollection tables = new TableSchema.TableCollection(); TableSchema.Table t = new TableSchema.Table("Test1"); t.AddColumn("Name", System.Data.DbType.String); t.AddColumn("Description", System.Data.DbType.String,5000); t.AddColumn("DateEntered", System.Data.DbType.DateTime,0, false, "getdate()"); AddSubSonicStateColumns(t); tables.Add(t); t = new TableSchema.Table("Test2"); t.AddColumn("Name", System.Data.DbType.String); t.AddColumn("Friend", System.Data.DbType.String); t.AddColumn("FriendInt", System.Data.DbType.Int32, 0,false,"50"); AddSubSonicStateColumns(t); tables.Add(t); Create(tables); } public override void Down() { Drop(new string[] { "Test1", "Test2" }); } }
More Code
You can also add/remove columns, and alter their settings (without dropping and losing data). This next bit of code shows not only how to alter data, but also why you'd use migrations in the first place - it's a road map of how your DB has changed over time:
public class Migration002:Migration { public override void Up() { //alter the description field - change to 1200 TableSchema.TableColumn col = DataService.GetSchema("Test1", "Northwind").GetColumn("Description"); col.MaxLength = 1200; //use AlterColumn to run an Alter statement - we don't want to save the schema //since that would overwrite what we're trying to do AlterColumn(col); //add a new column to Products DataService.GetSchema("Products", "Northwind").AddColumn("MyNewColumn",System.Data.DbType.DateTime); //add another one and set the default, as well as non-nullable DataService.GetSchema("Products", "Northwind").AddColumn("MaxInventory", System.Data.DbType.Int32,0,false,"100"); } public override void Down() { //the Down() method reverses what Up() did //reset Description to 9000 TableSchema.TableColumn col = DataService.GetSchema("Test1", "Northwind").GetColumn("Description"); col.MaxLength = 9000; AlterColumn(col); //remove MyNewColumn and MaxInventory RemoveColumn("Northwind", "Products", "MyNewColumn"); RemoveColumn("Northwind", "Products", "MaxInventory"); } }
How It All Works
There's a lot going on under the hood here, as you can imagine. To keep track of it all, we create a new table in your DB (we have to) called "SubSonic_Schema" and it has one field - "version". We set this to know which version the DB is, and it allows SubSonic to know which direction to take the Migration (up or down).
The tool that actually runs the migration is our command line tool, SubCommander. This tool not only runs migrations, it will also create the migration code file (with naming and everything) that's needed to run the migration itself (you can do this by hand if you like):
sonic.exe migration /create
To run the migration:
sonic.exe migrate
To run to a specific version:
sonic.exe migration /version 1
When you run a migration (as opposed to creating one), SubCommander will look for a folder called "Migrations" in the same directory that it's executing in. If you're running it in VS (like you do with the generators), this would be the root of your project.
All the files get pulled in, and the name of the migration file is analyzed - this part is CRUCIAL. We need to use some method, non-code based, to determine which version the migration represents. For now I've come up with:
VERSION_Descriptor.cs (or vb)
so a sample migration code file would be "001_InitialSchema.cs". The numeric part is for us, the descriptor part is for you. I'm still very open on this, but this is how Rails does it and if someone else already thought this stuff out...
Each code file is pulled in and SubSonic takes a look at the version (the first 3 characters) and decides which to use. Once that's determined, then the source is pulled from the code file, handed off to our compiler (the same bits that our generator uses), and (using Reflection) the Up() or Down() method gets invoked in the appropriate migration file. If your current version is 2 (for instance) and you have migration files 001_x through 009_x, Up() will be called on files 003_x through 009_x.
Once your DB is version 9, you can version back down to 3 by using the command
sonic.exe migrate /version 3
This will run the Down() method in code files 009_x through 004_x.
The cool thing about this process is that you can call your class and namespace whatever you like; it's the naming of the file that's important. Would love some comments on this since naming is the biggest issue we have and people have some whacky naming conventions. I'd like to be open on this - but in general I think I'll default to some stronger adherence to convention.
Finally - each provider is responsible for implementing the methods needed to create/alter/remove columns as well as create/alter/drop tables. So far I've extended only the SQL Server provider (and by extension the EntLib ones) - I'm hoping to get to the MySQL and others before we release this.
Everyday Use
To rewind a bit - the goal here is to give you a way to version your DB as you work, and give you a way to go backwards if needed. Also, this is a great way for teams to review how a DB was schemed and built, with comments and so forth that can be reviewed during a code-review cycle.
When you need to change your DB, you just "migrate" it to the next version - this is the thinking here. Rather than just a quick tweak in the designer, you track what you do in code.
Many people have asked "why not just let Subversion track your changes as you make them to one code file" and the answer is that this method doesn't allow you to go backwards, unless you Revert, which isn't the best option.
Left To Do
There's a lot left to do - specifically I want to make sure this is all transactional. I have some ideas for this - but the thing I really need to find out (which I don't know) is how well the providers we use (MySQL, SqlLite, etc) support DB Management transactions - in other words data transactions are pretty straightforward; I'm not so sure about schema changes.
Also, I want to be able to "Reverse Engineer" an existing DB into a migration file. This isn't very hard to do, but doing it right might take some time to figure out and I'd love your comments on this.
When? Did You Say When? What Was That?
I have no timeline just yet, though if you want to play around with the bits they will be committed tonight/tomorrow (I hope). I want to include this feature as part of our next major release, which will also include our REST Handler and our new query tool - so it will be a few months until we can get this all tightened up.
I'm doing what I can to crack this stuff out and Eric's getting married in October (oops... not sure if I'm supposed to tell people that) so he's sort of out of the loop for a bit. I know he'd love any presents you want to send him :).
I'm going to lean on the commit team to see if they can free up some time (Phil? Jon? Scott?) - is it OK to call out your dev team like this? Who cares... it's Open Source and we make the rules as we go don't we?
And let me state this clearly:
I'm always working the bug list, so YES I WILL fix the outstanding issues :):):):)
Lemme hear it! What do y'all want to see aside from bug fixes and an earlier release?
(Does Rails allow you to add data while doing a Migration??)
But VERY nice... I shall go play arround with it.
@Zack - Yes, Rails allows you to add data in your migrations. Once a table is created, you can use that model to add records.
Rails migrations are pretty lean and mean and to the point, this looks to be a lot of hand jiving so anything that can help reduce that is always nice. Sorry if I missed something that makes this idea stupid, I just skimmed and dashed.
You are doing a great job with Subsonic. I would like to thank you for your efforts to provide a great tool.
I would like to know, if you have a future in Subsonic, where you need to make/create a TempTable to have some sort of data.
I have this data, where user selects a rostercode from the Dropdownlist, than selects a date to view all the employees time sheet for a week of data.
The problem I am having is that If there is no data in the for requested date/dates, I get no results.
I am joining two tables. Because of this, I can't create a report.
I come up with a solution but I would like to have a short one. So, in Subsonic, is there a future where I can create a tmpTable to create the data to retrieve it for the reporting.
If you can let me know that would be great!
Thanks,
Atilla
Great work!
I am a great fan of subsonic. So far i've only be impressed at what could be done with it.
Up to now, your're reading SQL's metadata/schema to provide us with a "middle tier" model.
Now with migrations that same tier will kinda be able to change/update the only source of info it has.
What's the big picture (integration plan) about it?
I know the scope migrations is pretty big but...
Aren't you forgeting anything?
Table have constraints, triggers, indexes, relations. And the there's view's and procedures related to thoses.
All the objects above are working together to provide the best bang for buck!
Where will the migrations stand by this?
I have a quick question about the way Subsonic sends sql select commands.
After using SQL profiler, i've noticed that most of the queries are ending as exec sql commands.
Doesn't this affects performance / tuning / stats of SQL Server?
Also i noticed that if i bind a collection of objects representing a table that has foreign keys, each of the rows send another select cmd to db.
Is there a better approach in subsonic to limit cmds issued too db? I tried the Lazy stuff without looking at the code and it had no effect :-(
Yvan aka Vanof.
Good stuff. Here's a few questions:
What if I need to make a new, non-null column that will require a migration script?
From a Developers perspective, I want to see all of the values of a lookup table in one place, not spread out over a half dozen migrations. Is it possible to have them managed in a single place?
How can I Add a new Lookup value, run a process that will change "some" of dependent values to this new value?
How do you manage Stored Procedures, Views, and Functions? As a developer, I want to see only the latest version, edit it, and count on it being deployed with the next release. I don't want to search back through Migrations to find the latest version.
Cash Foley
Where are you up to with Migrations?
Have you decided on how you'll work the transactions?
I still think that we need some way to be able to start the migration from Subsonic instead of SubCommander. This way we could trigger the migration using an embedded logic.
Really looking forward to this.
Vanof.