Is this the right way to query a SQL Server CE table for a record, populating and returning a custom object?

StackOverflow https://stackoverflow.com/questions/20412042

Question

The code below works, but I'm wondering if it's more loquacious than necessary:

public static InventoryItem SelectLocalInventoryItem(string ID)
{
    const int ID_COL = 0;
    const int PACKSIZE_COL = 1;
    const int DESCRIPTION_COL = 2;
    const int DEPTDOTSUBDEPT_COL = 3;
    const int UNITCOST_COL = 4;
    const int UNITLIST_COL = 5;
    const int UPCCODE_COL = 6;
    const int UPCPACKSIZE_COL = 7;
    const int CRVID_COL = 8;

    var invItem = new InventoryItem();
    using (var conn = new SqlCeConnection(dataSource))
    {
        var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT * FROM InventoryItems WHERE Id = @IDVal";

        var IDParam = cmd.CreateParameter();
        IDParam.ParameterName = "@IdVal";
        IDParam.Value = ID;
        cmd.Parameters.Add(IDParam);

        conn.Open();
        cmd.Prepare();
        using (var reader = cmd.ExecuteReader())
        {
            if (reader.Read())
            {
                invItem.Id = reader.GetString(ID_COL);
                invItem.PackSize = reader.GetInt16(PACKSIZE_COL); 
                invItem.Description = reader.GetString(DESCRIPTION_COL);
                invItem.DeptDotSubdept = reader.GetDouble(DEPTDOTSUBDEPT_COL); 
                invItem.Unit_Cost = reader.GetDouble(UNITCOST_COL);
                invItem.Unit_List = reader.GetDouble(UNITLIST_COL);
                invItem.UPC_code = reader.GetString(UPCCODE_COL);
                invItem.UPC_pack_size = reader.GetInt16(UPCPACKSIZE_COL); 
                invItem.CRV_Id = reader.GetInt32(CRVID_COL);
            }
        }
        conn.Close();
        cmd.Dispose();
        return invItem;
    }
}

The table being queried is created like so:

using (var connection = new SqlCeConnection(dataSource))
{
    connection.Open();
    using (var command = new SqlCeCommand())
    {
        command.Connection = connection;

        if (TableExists(connection, "InventoryItems"))
        {
            command.CommandText = "DROP TABLE InventoryItems";
            command.ExecuteNonQuery();
        }
        command.CommandText = "CREATE TABLE InventoryItems (Id nvarchar(50) NOT 
            NULL, PackSize smallint NOT NULL, Description nvarchar(255), 
            DeptDotSubdept float, UnitCost float, UnitList float, UPCCode 
            nvarchar(50), UPCPackSize smallint, CRVId int);";
        command.ExecuteNonQuery();
    . . .
    }
}

The class is declared thusly:

public class InventoryItem
{
    public string Id { get; set; }
    public int PackSize { get; set; }
    public string Description { get; set; }
    public double DeptDotSubdept { get; set; }
    public double Unit_Cost { get; set; }
    public double Unit_List { get; set; }
    public string UPC_code { get; set; }
    public int UPC_pack_size { get; set; }
    public int CRV_Id { get; set; }
}

Is there an easier/quicker way to accomplish this, or do I really have to painstakingly manually assign each returned column to each class member?

UPDATE

I implemented Sergey K's suggestions, and here it is now:

public static InventoryItem SelectLocalInventoryItem(string ID)
{
    InventoryItem invItem = null;
    using (var conn = new SqlCeConnection(dataSource))
    {
        var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT * FROM InventoryItems WHERE Id = @IDVal";

        var IDParam = cmd.CreateParameter();
        IDParam.ParameterName = "@IdVal";
        IDParam.Value = ID;
        cmd.Parameters.Add(IDParam);

        conn.Open();
        cmd.Prepare();
        using (var reader = cmd.ExecuteReader())
        {
            if (reader.Read())
            {
                invItem = new InventoryItem
                {
                    Id = Convert.ToString(reader["Id"]),
                    PackSize = Convert.ToInt16(reader["PackSize"]), 
                    Description = Convert.ToString(reader["Description"]), 
                    DeptDotSubdept = Convert.ToDouble(reader["DeptDotSubdept"]),
                    Unit_Cost = Convert.ToDouble(reader["UnitCost"]),
                    Unit_List = Convert.ToDouble(reader["UnitList"]),
                    UPC_code = Convert.ToString(reader["UPCCode"]),
                    UPC_pack_size = Convert.ToInt16(reader["UPCPackSize"]),
                    CRV_Id = Convert.ToInt32(reader["CRVId"])
                };
            }
        }
        return invItem;
    }
}

UPDATE 2

For the record/posterity, here is a related method that returns all the values, rather than a single "record"/class instance:

public static List<InventoryItem> SelectLocalInventoryItems()
{
    List<InventoryItem> invItems = new List<InventoryItem>();
    using (var conn = new SqlCeConnection(dataSource))
    {
        var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT * FROM InventoryItems";
        conn.Open();
        cmd.Prepare();
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                var invItem = new InventoryItem
                {
                    Id = Convert.ToString(reader["Id"]),
                    PackSize = Convert.ToInt16(reader["PackSize"]),
                    Description = Convert.ToString(reader["Description"]),
                    DeptDotSubdept = Convert.ToDouble(reader["DeptDotSubdept"]),
                    Unit_Cost = Convert.ToDouble(reader["UnitCost"]),
                    Unit_List = Convert.ToDouble(reader["UnitList"]),
                    UPC_code = Convert.ToString(reader["UPCCode"]),
                    UPC_pack_size = Convert.ToInt16(reader["UPCPackSize"]),
                    CRV_Id = Convert.ToInt32(reader["CRVId"])
                };
                invItems.Add(invItem);
            }
        }
    }
    return invItems;
}

UPDATE 3

This is an update of update 2, following ctacke's suggestion:

public static List<HHSUtils.InventoryItem> SelectLocalInventoryItemsTableDirect()
{
    var invItems = new List<HHSUtils.InventoryItem>();
    using (var conn = new SqlCeConnection(dataSource))
    {
        conn.Open();
        SqlCeCommand cmd = conn.CreateCommand();
        cmd.CommandType = CommandType.TableDirect;
        cmd.CommandText = "InventoryItems";
        using (SqlCeResultSet rs cmd.ExecuteResultSet(ResultSetOptions.Scrollable))
        {
            cmd.Prepare();
            while (rs.Read())
            {
                var invItem = new HHSUtils.InventoryItem
                {
                    Id = Convert.ToString(rs["Id"]),
                    PackSize = Convert.ToInt16(rs["PackSize"]),
                    Description = Convert.ToString(rs["Description"]),
                    DeptDotSubdept = Convert.ToDouble(rs["DeptDotSubdept"]),
                    Unit_Cost = Convert.ToDouble(rs["UnitCost"]),
                    Unit_List = Convert.ToDouble(rs["UnitList"]),
                    UPC_code = Convert.ToString(rs["UPCCode"]),
                    UPC_pack_size = Convert.ToInt16(rs["UPCPackSize"]),
                    CRV_Id = Convert.ToInt32(rs["CRVId"])
                };
                invItems.Add(invItem);
            }
        }
    }
    return invItems;
}

I don't know yet if ResultSetOptions.Scrollable is the best property to use here, though... This msdn article makes me only slightly wiser.

UPDATE 4

The TableDirect change seems to be good; so I tried to implement the GetValues suggestion, too. But changing this code:

    using (SqlCeResultSet rs = cmd.ExecuteResultSet(ResultSetOptions.Scrollable))
    {
        cmd.Prepare();
        while (rs.GetValues())
        {
            var invItem = new HHSUtils.InventoryItem
            {
                Id = Convert.ToString(rs["Id"]),
                PackSize = Convert.ToInt16(rs["PackSize"]),
                Description = Convert.ToString(rs["Description"]),
                DeptDotSubdept = Convert.ToDouble(rs["DeptDotSubdept"]),
                Unit_Cost = Convert.ToDouble(rs["UnitCost"]),
                Unit_List = Convert.ToDouble(rs["UnitList"]),
                UPC_code = Convert.ToString(rs["UPCCode"]),
                UPC_pack_size = Convert.ToInt16(rs["UPCPackSize"]),
                CRV_Id = Convert.ToInt32(rs["CRVId"])
            };
            invItems.Add(invItem);
        }
    }
}

...to this:

    using (SqlCeResultSet rs = cmd.ExecuteResultSet(ResultSetOptions.Scrollable))
    {
        cmd.Prepare();
        Object[] values = new Object[rs.FieldCount];
        int fieldCount = rs.GetValues(values);
        for (int i = 0; i < fieldCount; i++)
        {
            var invItem = new HHSUtils.InventoryItem
            {
                Id = Convert.ToString(rs["Id"]),
                PackSize = Convert.ToInt16(rs["PackSize"]),
                Description = Convert.ToString(rs["Description"]),
                DeptDotSubdept = Convert.ToDouble(rs["DeptDotSubdept"]),
                Unit_Cost = Convert.ToDouble(rs["UnitCost"]),
                Unit_List = Convert.ToDouble(rs["UnitList"]),
                UPC_code = Convert.ToString(rs["UPCCode"]),
                UPC_pack_size = Convert.ToInt16(rs["UPCPackSize"]),
                CRV_Id = Convert.ToInt32(rs["CRVId"])
            };
            invItems.Add(invItem);
        }
    }
}

...fails on the "int fieldCount = rs.GetValues(values);" line, with "No data exists for the row/column"

UPDATE 5

In response to ctacke: So it's simply a matter of adding "Object[] values = new Object[rs.FieldCount];" before the while and "rs.GetValues(values);" after it, like so:

using (SqlCeResultSet rs = cmd.ExecuteResultSet(ResultSetOptions.Scrollable))
{
    cmd.Prepare();
    Object[] values = new Object[rs.FieldCount];
    while (rs.Read())
    {
        rs.GetValues(values);
        var invItem = new HHSUtils.InventoryItem
        {
            . . .

? It seems to work...

UPDATE 6

For posterity, this seems to be good form and work well for a "Select *" on a SQL Server CE table:

public static List<HHSUtils.Department> SelectLocalDepartments()
{
    var departments = new List<HHSUtils.Department>();
    using (var conn = new SqlCeConnection(dataSource))
    {
        conn.Open();
        SqlCeCommand cmd = conn.CreateCommand();
        cmd.CommandType = CommandType.TableDirect;
        cmd.CommandText = "Departments";
        using (SqlCeResultSet rs = cmd.ExecuteResultSet(ResultSetOptions.None))
        {
            var values = new Object[rs.FieldCount];
            while(rs.Read())
            {
                rs.GetValues(values);
                var dept = new HHSUtils.Department
                { 
                    Id = Convert.ToInt16(rs["Id"]),
                    DeptNumber = Convert.ToInt16(rs["DeptNum"]),
                    DeptName = Convert.ToString(rs["DepartmentName"]),
                };
                departments.Add(dept);
            }
        }
    }
    return departments;
}
Was it helpful?

Solution

I see few things you can do better.

  1. Why not to use column name to get a value from reader? Something like

Convert.ToDouble(reader["DeptDotSubdept"]); I don't see any sense to have constants to identify the column number in scope of your method.

  1. You can use object initializer to instantiate your object.

    if (reader.Read())

    {
        invItem = new InventoryItem{
          Id = Convert.ToString(reader["Id"]),
        .....
        };
    }
    
  2. Return null reference if record is not found.

  3. If you know what is using do, you might don't want to add this lines

    conn.Close(); cmd.Dispose();

OTHER TIPS

It's worth noting that if you're doing this across a lot of rows, this can be improved for speed greatly. There are two improvement paths:

  1. Modest improvement can be made by first caching the numeric field ordinals before iterating the rows, using reader.GetValues to pull the entire data row, then accessing the resulting array with the cached ordinals.

The reason this is better is twofold. First, it skips the need for the reader to always look up the ordinal based on the name you provided, and two it doesn't require a roundtrip to the data for each field you want.

  1. An order of magnitude improvement can be had by just opening the table using TableDirect instead of a SQL query and then doing the suggestions in #1.

EDIT

Something along these lines:

using (var rs = cmd.ExecuteResultSet())
{
    var fieldCount = rs.fieldCount;
    var values = new Object[rs.FieldCount];

    // cache your ordinals here using rs.GetOrdinal(fieldname)
    var ID_ORDINAL = rs.GetOrdinal("Id");
    // etc

    while(rs.Read())
    {
        rs.GetValues(values);

        var invItem = new HHSUtils.InventoryItem
        {
            Id = (string)values[ID_ORDINAL],
            PackSize = (short)values[PACK_SIZE_ORDINAL],
            // etc
        };
        invItems.Add(invItem);
    }
}

EDIT 2

It's probably worth noting that if you were using something like the OpenNETCF ORM, the code to do the above would look like this:

invItems = store.Select<InventoryItem>();

That's it, just one line. And it would have used TableDirect by default.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top