With OrmLite, is there a way to automatically update table schema when my POCO is modified?
Asked Answered
H

6

9

Can OrmLite recognize differences between my POCO and my schema and automatically add (or remove) columns as necessary to force the schema to remain in sync with my POCO?

If this ability doesn't exist, is there way for me to query the db for table schema so that I may manually perform the syncing? I found this, but I'm using the version of OrmLite that installs with ServiceStack and for the life of me, I cannot find a namespace that has the TableInfo classes.

Harem answered 3/1, 2013 at 15:57 Comment(2)
Wait. Are you talking about ORMLite the JAVA orm or ormlite-servicestack: code.google.com/p/servicestack/wiki/OrmLiteEndothelioma
I'm specifically using ormlite-servicestack and I assumed that the code bases would have identical features, even they were written in different languages. Wrong assumption on my part. Thank you for the clarification.Harem
L
4

No there is no current support for Auto Migration of RDBMS Schema's vs POCOs in ServiceStack's OrmLite.

There are currently a few threads being discussed in OrmLite's issues that are exploring the different ways to add this.

Lacreshalacrimal answered 4/1, 2013 at 5:58 Comment(3)
Thank you @Lacreshalacrimal for your quick and accurate replies.Harem
@Lacreshalacrimal the link for OrmLite's issues is broken.Beckman
Is there an Update for auto migration support?Nippers
S
11

I created an extension method to automatically add missing columns to my tables. Been working great so far. Caveat: the code for getting the column names is SQL Server specific.

namespace System.Data
{
    public static class IDbConnectionExtensions
    {
        private static List<string> GetColumnNames(IDbConnection db, string tableName)
        {
            var columns = new List<string>();
            using (var cmd = db.CreateCommand())
            {
                cmd.CommandText = "exec sp_columns " + tableName;
                var reader = cmd.ExecuteReader();
                while (reader.Read())
                {
                    var ordinal = reader.GetOrdinal("COLUMN_NAME");
                    columns.Add(reader.GetString(ordinal));
                }
                reader.Close();
            }
            return columns;
        }

        public static void AlterTable<T>(this IDbConnection db) where T : new()
        {
            var model = ModelDefinition<T>.Definition;

            // just create the table if it doesn't already exist
            if (db.TableExists(model.ModelName) == false)
            {
                db.CreateTable<T>(overwrite: false);
                return;
            }

            // find each of the missing fields
            var columns = GetColumnNames(db, model.ModelName);
            var missing = ModelDefinition<T>.Definition.FieldDefinitions
                .Where(field => columns.Contains(field.FieldName) == false)
                .ToList();

            // add a new column for each missing field
            foreach (var field in missing)
            {
                var alterSql = string.Format("ALTER TABLE {0} ADD {1} {2}", 
                    model.ModelName,
                    field.FieldName, 
                    db.GetDialectProvider().GetColumnTypeDefinition(field.FieldType)
                    );
                Console.WriteLine(alterSql);
                db.ExecuteSql(alterSql);
            }
        }
    }
}
Swob answered 28/3, 2013 at 17:49 Comment(2)
Nice solution. One notice: db.GetDialectProvider().GetColumnTypeDefinition(field.FieldType) here you're getting default value for the type. Type may have attributes like [StringLength(50)], so if you don't want to miss them you should check for attributes and their values to set right type in DB.Ploch
As an update to Scott's proposal and Ivan's comments, the following statements are available in recent versions of ServiceStack that can be used in the foreach loop: var alterSql = db.GetDialectProvider().ToAddColumnStatement(typeof(T), field); This takes into account string length, nvarchar, etc.Claybourne
L
4

No there is no current support for Auto Migration of RDBMS Schema's vs POCOs in ServiceStack's OrmLite.

There are currently a few threads being discussed in OrmLite's issues that are exploring the different ways to add this.

Lacreshalacrimal answered 4/1, 2013 at 5:58 Comment(3)
Thank you @Lacreshalacrimal for your quick and accurate replies.Harem
@Lacreshalacrimal the link for OrmLite's issues is broken.Beckman
Is there an Update for auto migration support?Nippers
S
2

Here is a slightly modified version of code from cornelha to work with PostgreSQL. Removed this fragment

        //private static List<string> GetColumnNames(object poco)
        //{
        //    var list = new List<string>();
        //    foreach (var prop in poco.GetType().GetProperties())
        //    {
        //        list.Add(prop.Name);
        //    }
        //    return list;
        //}

and used IOrmLiteDialectProvider.NamingStrategy.GetTableName and IOrmLiteDialectProvider.NamingStrategy.GetColumnName methods to convert table and column names from PascalNotation to this_kind_of_notation used by OrmLite when creating tables in PostgreSQL.

    public static class IDbConnectionExtensions
    {
        private static List<string> GetColumnNames(IDbConnection db, string tableName, IOrmLiteDialectProvider provider)
        {
            var columns = new List<string>();
            using (var cmd = db.CreateCommand())
            {
                cmd.CommandText = getCommandText(tableName, provider);
                var tbl = new DataTable();
                tbl.Load(cmd.ExecuteReader());
                for (int i = 0; i < tbl.Columns.Count; i++)
                {
                    columns.Add(tbl.Columns[i].ColumnName);
                }

            }
            return columns;
        }

        private static string getCommandText(string tableName, IOrmLiteDialectProvider provider)
        {

            if (provider == PostgreSqlDialect.Provider)

                return string.Format("select * from {0} limit 1", tableName);
            else return string.Format("select top 1 * from {0}", tableName);
        }

        public static void AlterTable<T>(this IDbConnection db, IOrmLiteDialectProvider provider) where T : new()
        {
            var model = ModelDefinition<T>.Definition;
            var table = new T();
            var namingStrategy = provider.NamingStrategy;
            // just create the table if it doesn't already exist
            var tableName = namingStrategy.GetTableName(model.ModelName);
            if (db.TableExists(tableName) == false)
            {
                db.CreateTable<T>(overwrite: false);
                return;
            }

            // find each of the missing fields
            var columns = GetColumnNames(db, model.ModelName, provider);
            var missing = ModelDefinition<T>.Definition.FieldDefinitions
                                            .Where(field => columns.Contains(namingStrategy.GetColumnName(field.FieldName)) == false)
                                            .ToList();

            // add a new column for each missing field
            foreach (var field in missing)
            {
                var columnName = namingStrategy.GetColumnName(field.FieldName);
                var alterSql = string.Format("ALTER TABLE {0} ADD COLUMN {1} {2}",
                                             tableName,
                                             columnName,
                                             db.GetDialectProvider().GetColumnTypeDefinition(field.FieldType)
                    );
                Console.WriteLine(alterSql);
                db.ExecuteSql(alterSql);
            }
        }
    }
Stung answered 24/11, 2013 at 20:7 Comment(0)
P
2

I implemented an UpdateTable function. The basic idea is:

  1. Rename current table on database.
  2. Let OrmLite create the new schema.
  3. Copy the relevant data from the old table to the new.
  4. Drop the old table.

Github Repo: https://github.com/peheje/Extending-NServiceKit.OrmLite

Condensed code:

public interface ISqlProvider
    {
        string RenameTableSql(string currentName, string newName);
        string GetColumnNamesSql(string tableName);
        string InsertIntoSql(string intoTableName, string fromTableName, string commaSeparatedColumns);
        string DropTableSql(string tableName);
    }

public static void UpdateTable<T>(IDbConnection connection, ISqlProvider sqlProvider) where T : new()
        {
            connection.CreateTableIfNotExists<T>();
            var model = ModelDefinition<T>.Definition;
            string tableName = model.Name;
            string tableNameTmp = tableName + "Tmp";
            string renameTableSql = sqlProvider.RenameTableSql(tableName, tableNameTmp);
            connection.ExecuteNonQuery(renameTableSql);

            connection.CreateTable<T>();

            string getModelColumnsSql = sqlProvider.GetColumnNamesSql(tableName);
            var modelColumns = connection.SqlList<string>(getModelColumnsSql);
            string getDbColumnsSql = sqlProvider.GetColumnNamesSql(tableNameTmp);
            var dbColumns = connection.SqlList<string>(getDbColumnsSql);

            List<string> activeFields = dbColumns.Where(dbColumn => modelColumns.Contains(dbColumn)).ToList();

            string activeFieldsCommaSep = ListToCommaSeparatedString(activeFields);
            string insertIntoSql = sqlProvider.InsertIntoSql(tableName, tableNameTmp, activeFieldsCommaSep);

            connection.ExecuteSql(insertIntoSql);

            string dropTableSql = sqlProvider.DropTableSql(tableNameTmp);
            //connection.ExecuteSql(dropTableSql);  //maybe you want to clean up yourself, else uncomment
        }

        private static String ListToCommaSeparatedString(List<String> source)
        {
            var sb = new StringBuilder();
            for (int i = 0; i < source.Count; i++)
            {
                sb.Append(source[i]);
                if (i < source.Count - 1)
                {
                    sb.Append(", ");
                }
            }
            return sb.ToString();
        }
    }

MySql implementation:

public class MySqlProvider : ISqlProvider
    {
        public string RenameTableSql(string currentName, string newName)
        {
            return "RENAME TABLE `" + currentName + "` TO `" + newName + "`;";
        }

        public string GetColumnNamesSql(string tableName)
        {
            return "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" + tableName + "';";
        }

        public string InsertIntoSql(string intoTableName, string fromTableName, string commaSeparatedColumns)
        {
            return "INSERT INTO `" + intoTableName + "` (" + commaSeparatedColumns + ") SELECT " + commaSeparatedColumns + " FROM `" + fromTableName + "`;";
        }

        public string DropTableSql(string tableName)
        {
            return "DROP TABLE `" + tableName + "`;";
        }
    }

Usage:

 using (var db = dbFactory.OpenDbConnection())
 {
     DbUpdate.UpdateTable<SimpleData>(db, new MySqlProvider());
 }

Haven't tested with FKs. Can't handle renaming properties.

Pomace answered 27/4, 2015 at 17:49 Comment(0)
I
1

I needed to implement something similiar and found the post by Scott very helpful. I decided to make a small change which will make it much more agnostic. Since I only use Sqlite and MSSQL, I made the getCommand method very simple, but can be extended. I used a simple datatable to get the columns. This solution works perfectly for my requirements.

    public static class IDbConnectionExtensions
{
    private static List<string> GetColumnNames(IDbConnection db, string tableName,IOrmLiteDialectProvider provider)
    {
        var columns = new List<string>();
        using (var cmd = db.CreateCommand())
        {
            cmd.CommandText = getCommandText(tableName, provider);
            var tbl = new DataTable();
            tbl.Load(cmd.ExecuteReader());
            for (int i = 0; i < tbl.Columns.Count; i++)
            {
                columns.Add(tbl.Columns[i].ColumnName);
            }

        }
        return columns;
    }

    private static string getCommandText(string tableName,  IOrmLiteDialectProvider provider)
    {

        if(provider ==  SqliteDialect.Provider)

        return string.Format("select * from {0} limit 1", tableName);
        else return string.Format("select top 1 * from {0}", tableName);
    }

    private static List<string> GetColumnNames(object poco)
    {
        var list = new List<string>();
        foreach (var prop in poco.GetType().GetProperties())
        {
            list.Add(prop.Name);
        }
        return list;
    }

    public static void AlterTable<T>(this IDbConnection db, IOrmLiteDialectProvider provider) where T : new()
    {
        var model = ModelDefinition<T>.Definition;
        var table = new T();
        // just create the table if it doesn't already exist
        if (db.TableExists(model.ModelName) == false)
        {
            db.CreateTable<T>(overwrite: false);
            return;
        }

        // find each of the missing fields
        var columns = GetColumnNames(db, model.ModelName,provider);
        var missing = ModelDefinition<T>.Definition.FieldDefinitions
                                        .Where(field => columns.Contains(field.FieldName) == false)
                                        .ToList();

        // add a new column for each missing field
        foreach (var field in missing)
        {
            var alterSql = string.Format("ALTER TABLE {0} ADD {1} {2}",
                                         model.ModelName,
                                         field.FieldName,
                                         db.GetDialectProvider().GetColumnTypeDefinition(field.FieldType)
                );
            Console.WriteLine(alterSql);
            db.ExecuteSql(alterSql);
        }
    }
}
Illyrian answered 16/10, 2013 at 6:51 Comment(1)
Since I cannot yet comment on others posts, in response to user44: Now all we need is a method to copy existing data to a temp table, upgrade the table and copy the data back. Few changes based on the provider and have ourselves a winner here.Illyrian
T
1

So I took user44 answer, and modified the AlterTable method to make it a bit more efficient. Instead of looping and running one SQL query per field/column, I merge it into one with some simple text parsing (MySQL commands!).

        public static void AlterTable<T>(this IDbConnection db, IOrmLiteDialectProvider provider) where T : new()
        {
            var model = ModelDefinition<T>.Definition;
            var table = new T();
            var namingStrategy = provider.NamingStrategy;
            // just create the table if it doesn't already exist
            var tableName = namingStrategy.GetTableName(model.ModelName);
            if (db.TableExists(tableName) == false)
            {
                db.CreateTable<T>(overwrite: false);
                return;
            }

            // find each of the missing fields
            var columns = GetColumnNames(db, model.ModelName, provider);
            var missing = ModelDefinition<T>.Definition.FieldDefinitions
                                            .Where(field => columns.Contains(namingStrategy.GetColumnName(field.FieldName)) == false)
                                            .ToList();
            string alterSql = "";
            string addSql = "";
            // add a new column for each missing field
            foreach (var field in missing)
            {
                var alt = db.GetDialectProvider().ToAddColumnStatement(typeof(T), field); // Should be made more efficient, one query for all changes instead of many
                int index = alt.IndexOf("ADD ");
                alterSql = alt.Substring(0, index);
                addSql += alt.Substring(alt.IndexOf("ADD COLUMN")).Replace(";", "") + ", ";
            }
            if (addSql.Length > 2)
                addSql = addSql.Substring(0, addSql.Length - 2);
            string fullSql = alterSql + addSql;
            Console.WriteLine(fullSql);
            db.ExecuteSql(fullSql);
        }
Tops answered 20/4, 2018 at 12:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.