如何将列表对象从 C# 传递到 Oracle 存储过程?

How To Pass a List Object From C# to an Oracle Stored Procedure?

我正在尝试将列表对象从我的 C# WebService 方法发送到我在 Oracle 中的存储过程。

在此处发帖之前,我已经尝试了所有建议的重复链接。这是我到目前为止所取得的成就:

我目前正在使用以下设置:

请记住,Oracle.ManagedDataAccess 18.6.0 版本不包含旧示例中建议的 OracleDbType.Array


        public class Automobile
        {
            public string Make { get; set; }
            public string Model { get; set; }
            public string Year { get; set; }
            public string Country { get; set; }
        }
        using Oracle.ManagedDataAccess.Client;
        using Oracle.ManagedDataAccess.Types;

        [WebMethod(EnableSession = true)]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public string InsertCars(List<Automobile> myCars, int userID)
        {
            DataSet dataSet = new DataSet();

            using (OracleConnection sqlConnection = new OracleConnection(OracleDBConnection))
            {
                using (OracleCommand sqlCommand = new OracleCommand("sp_InsertCars", sqlConnection))
                {
                    sqlConnection.Open();
                    sqlCommand.CommandType = CommandType.StoredProcedure;

                    sqlCommand.Parameters.Add(
                        new OracleParameter
                        {
                            CollectionType = OracleCollectionType.PLSQLAssociativeArray,
                            Direction = ParameterDirection.Input,
                            ParameterName = "p_CarList",
                            UdtTypeName = "tt_Automobile",
                            Size = myCars.Count,
                            Value = myCars.ToArray()
                        }
                    );

                    sqlCommand.Parameters.Add(
                        new OracleParameter
                        {
                            OracleDbType = OracleDbType.Int32,
                            Direction = ParameterDirection.Input,
                            ParameterName = "p_UserID",
                            Value = userID
                        }
                    );

                    sqlCommand.Parameters.Add(
                        new OracleParameter
                        {
                            OracleDbType = OracleDbType.RefCursor,
                            Direction = ParameterDirection.Output,
                            ParameterName = "o_Cursor"
                        }
                    );

                    using (OracleDataAdapter sqlAdapter = new OracleDataAdapter(sqlCommand))
                    {
                        sqlAdapter.SelectCommand = sqlCommand;
                        sqlAdapter.Fill(dataSet);
                    }
                }

                return JsonConvert.SerializeObject(dataSet);
            }
        }

        CREATE TABLE tblCars
        (
            RecordID INT GENERATED BY DEFAULT  AS IDENTITY NOMINVALUE NOMAXVALUE INCREMENT BY 1 START WITH 1 NOCACHE NOCYCLE NOORDER,
            Make     NVARCHAR2(100)   NULL,
            Model    NVARCHAR2(100)   NULL,
            Year     NVARCHAR2(4)     NULL,
            Country  NVARCHAR2(100)   NULL,
            UserID   INT              NULL
        );

        CREATE OR REPLACE TYPE ot_Automobile AS OBJECT
        ( 
            Make varchar2(100),
            Model varchar2(100),
            Year varchar2(4),
            Country varchar2(100)
        );

        CREATE OR REPLACE TYPE tt_Automobile AS TABLE OF ot_Automobile;

        CREATE OR REPLACE PROCEDURE sp_InsertCars 
        (
            p_CarList In tt_Automobile,
            p_UserID In integer,
            o_Cursor Out Sys_RefCursor
        )
        AS
        BEGIN
            DBMS_Output.Enable;

            For RowItem In (Select * From Table(p_CarList))
            Loop
            Insert Into tblCars 
            (
                Make, 
                Model, 
                Year, 
                Country, 
                UserID
            )
            Values(
                RowItem.Make,
                RowItem.Model,
                RowItem.Year,
                RowItem.Country,
                p_UserID
            );        
            End Loop;

            -- Return our results after insert
            Open o_Cursor For
            Select Make, Model, Year, Country From tblCars Where UserID = p_UserID;

        EXCEPTION
            When Others Then
            DBMS_Output.Put_Line('SQL Error: ' || SQLERRM);        

        END sp_InsertCars;

        COMMIT
        /

结果应该允许我将我的数组对象从我的 WebService WebMethod 传递到我的 Oracle 存储过程,然后遍历数组的每个项目以执行插入。

这是我尝试传入的数据示例。

请参考以下link设置ODAC Setup Ref and use follwing link to get the ODAC

using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;
using System;
using System.Data;

namespace Strace_CustomTypes
{
    class Program
    {
        static void Main(string[] args)
        {
            // Setup Ref - https://o7planning.org/en/10509/connecting-to-oracle-database-using-csharp-without-oracle-client
            // ODAC 64bit ODAC122010Xcopy_x64.zip - https://www.oracle.com/technetwork/database/windows/downloads/index-090165.html
            // .Net Framework 4

            // 'Connection string' to connect directly to Oracle.
            string connString = "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=0.0.0.0)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=SIT)));Password=PASSWORD;User ID=USERID";


            OracleConnection straceOracleDBConn = new OracleConnection(connString);
            OracleCommand cmd = new OracleCommand("PKG_TEMP.TEST_ARRAY", straceOracleDBConn);
            cmd.CommandType = CommandType.StoredProcedure;

            try
            {
                straceOracleDBConn.Open();

                CustomVarray pScanResult = new CustomVarray();

                pScanResult.Array = new string[] { "hello", "world" };

                OracleParameter param = new OracleParameter();
                param.OracleDbType = OracleDbType.Array;
                param.Direction = ParameterDirection.Input;

                param.UdtTypeName = "USERID.VARCHAR2_ARRAY";
                param.Value = pScanResult;
                cmd.Parameters.Add(param);

                cmd.ExecuteNonQuery();
            }
            catch (Exception ex)
            {

                Console.WriteLine($"Error: {ex.Message} {Environment.NewLine} {ex.StackTrace}");
            }
            finally
            {
                straceOracleDBConn.Close();
                cmd.Dispose();
                straceOracleDBConn.Dispose();
            }

            Console.WriteLine("Press any key to exit");
            Console.ReadLine();
        }
    }

    //Ref https://www.codeproject.com/Articles/33829/How-to-use-Oracle-11g-ODP-NET-UDT-in-an-Oracle-Sto
    public class CustomVarray : IOracleCustomType, INullable
    {
        [OracleArrayMapping()]
        public string[] Array;

        private OracleUdtStatus[] m_statusArray;
        public OracleUdtStatus[] StatusArray
        {
            get
            {
                return this.m_statusArray;
            }
            set
            {
                this.m_statusArray = value;
            }
        }

        private bool m_bIsNull;

        public bool IsNull
        {
            get
            {
                return m_bIsNull;
            }
        }

        public static CustomVarray Null
        {
            get
            {
                CustomVarray obj = new CustomVarray();
                obj.m_bIsNull = true;
                return obj;
            }
        }


        public void FromCustomObject(OracleConnection con, IntPtr pUdt)
        {
            OracleUdt.SetValue(con, pUdt, 0, Array, m_statusArray);
        }

        public void ToCustomObject(OracleConnection con, IntPtr pUdt)
        {
            object objectStatusArray = null;
            Array = (string[])OracleUdt.GetValue(con, pUdt, 0, out objectStatusArray);
            m_statusArray = (OracleUdtStatus[])objectStatusArray;
        }
    }

    [OracleCustomTypeMapping("USERID.VARCHAR2_ARRAY")]
    public class CustomVarrayFactory : IOracleArrayTypeFactory, IOracleCustomTypeFactory
    {
        public Array CreateArray(int numElems)
        {
            return new string[numElems];
        }

        public IOracleCustomType CreateObject()
        {
            return new CustomVarray();
        }

        public Array CreateStatusArray(int numElems)
        {
            return new OracleUdtStatus[numElems];
        }
    }
}

这个答案取决于商业套餐,但如果你像我一样绝望,它是非常合理的 300 美元(大约 2020 年第四季度)的救命稻草......球探荣誉,我不是托儿

DevArt's Oracle provider 可以优雅地将对象列表传递给存储过程...它确实有效....net core 3.1 兼容,在 linux 上测试,不依赖于本机 oracle 客户端...根据链接论坛 post

,查看我对下面的工作控制台应用程序示例的看法

DevArt 的“OracleType.GetObjectType()”API 使 UDT 编组的一部分对我们来说非常微不足道……比现有的非托管 ODP table 类型支持更容易理解我已经在那里看到多年的样品

战略考虑 - 如果您已经有一个基于 oracle 提供程序的相当大的代码库,请考虑只保留所有代码 as-is 并且只对专门 table 类型的新依赖项进行回归测试确实需要支持

短而甜的样品,可快速消化

using System;
using System.Data;
using Devart.Data.Oracle;
namespace ConsoleApp1
{
    class Program
    {
        private static int oraTable;

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //good docs:
            //direct connection: https://www.devart.com/dotconnect/oracle/docs/StoredProcedures-OracleCommand.html
            //linux licensing: https://www.devart.com/dotconnect/oracle/docs/?LicensingStandard.html
            using OracleConnection db = new OracleConnection("{insert yours}");

            //devart trial licensing nutshell... on WINDOWS, download & run their installer...
            //the only thing you really need from that install is the license key file dropped here: 
            //   %programdata%\DevArt\License\Devart.Data.Oracle.key
            // then just go nuget the "Devart.Data.Oracle" package reference
            //on Windows, the trial license file gets automatically picked up by their runtime
            //if you're on Linux, basically just read their good instructions out there
            //i've just tested it on WSL so far and plan to try on Azure linux app svc within next day
            //db.ConnectionString = "license key=trial:Devart.Data.Oracle.key;" + db.ConnectionString;

            db.Direct = true; //nugget: crucial!! https://www.devart.com/dotconnect/oracle/docs/DirectMode.html
            db.Open();

            var cmd = db.CreateCommand("UserPermissions_u", CommandType.StoredProcedure);
            cmd.DeriveParameters();
            //tblParm.OracleDbType = OracleDbType.Table;

            //passing "table" type proc parm example: https://forums.devart.com/viewtopic.php?t=22243
            var obj = new OracleObject(OracleType.GetObjectType("UserPerm", db));
            var tbl = new OracleTable(OracleType.GetObjectType("UserPerms", db));

            obj["UserPermissionId"] = "sR1CKjKYSKvgU90GUgqq+w==";
            obj["adv"] = 1;
            tbl.Add(obj);
            cmd.Parameters["IN_Email"].Value = "banderson@kingcounty.gov";
            cmd.Parameters["IN_Permissions"].Value = tbl;
            cmd.ExecuteNonQuery();

            //"i can't believe it's not butter!" -me, just now =)
        }
    }
}

对应的 oracle 数据库定义:

create or replace type UserPerm as object ( UserPermissionId varchar2(24), std number(1), adv number(1)  );
create or replace type UserPerms as table of UserPerm;

create or replace PROCEDURE UserPermissions_u (
  IN_Email IN varchar2,
  IN_Permissions IN UserPerms
) is 

dummyvar number default 0;

begin

select count(*) into dummyvar from table(IN_Permissions);

end;
/

更详尽的镜头一般反映入站对象以像 OP 的请求一样滋润 proc parms...谨慎,需要 testing/bullet-proofing...如果有人,我希望得到更好的选择关心分享

using System;
using System.Data;
using Devart.Data.Oracle;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections;

namespace ConsoleApp1
{
    public class User
    {
        public string Email { get; set; }
        public List<UserPermissionEffective> Permissions { get; set; }
    }

    public class UserPermissionEffective
    {
        public string UserPermissionId { get; set; }
        public string Email { get; set; }
        public bool Std { get; set; }
        public bool Adv { get; set; }
        public string Mod { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            var dto = new User { Email = "testy@mctesterson.com", Permissions = new List<UserPermissionEffective> {
                new UserPermissionEffective { UserPermissionId = "1", Std = false, Adv = true },
                new UserPermissionEffective { UserPermissionId = "2", Std = true, Adv = false }
            } };

            if (dto == null) return;

            //good docs:
            //direct connection: https://www.devart.com/dotconnect/oracle/docs/StoredProcedures-OracleCommand.html
            //linux licensing: https://www.devart.com/dotconnect/oracle/docs/?LicensingStandard.html
            var dbstring = Environment.GetEnvironmentVariable("dbstring");
            using OracleConnection db = new OracleConnection(dbstring);
            db.ConnectionString = "license key=trial:Devart.Data.Oracle.key;" + db.ConnectionString;
            db.Direct = true; //nugget: crucial!! https://www.devart.com/dotconnect/oracle/docs/DirectMode.html
            db.Open();

            var cmd = db.CreateCommand("UserPermissions_u", CommandType.StoredProcedure);
            cmd.DeriveParameters();

            //regex gets everything following the last underscore. e.g. INOUT_PARMNAME yields PARMNAME
            var regex = new Regex(@"([^_\W]+)$", RegexOptions.Compiled);

            //get the inboud model's root properties
            var dtoProps = dto.GetType().GetProperties();

            //loop over all parms assigning model properties values by name 
            //going by parms as the driver versus object properties to favor destination over source
            //since we often ignore some superfluous inbound properties
            foreach (OracleParameter parm in cmd.Parameters)
            {
                var cleanParmName = regex.Match(parm.ParameterName).Value.ToUpper();
                var dtoPropInfo = dtoProps.FirstOrDefault(prop => prop.Name.ToUpper() == cleanParmName);

                //if table type, then drill into the nested list
                if (parm.OracleDbType == OracleDbType.Table)
                {
                    //the type we're assigning from must be a list
                    //
                    Assert.IsTrue(typeof(IEnumerable).IsAssignableFrom(dtoPropInfo.PropertyType));

                    var listProperty = (dtoPropInfo.GetValue(dto) as IEnumerable<Object>).ToArray();
                    //don't bother further logic if the list is empty
                    if (listProperty.Length == 0) return;

                    //get the oracle table & item Udt's to be instanced and hydrated from the inbound dto
                    var tableUdt = OracleType.GetObjectType(parm.ObjectTypeName, db);
                    var itemUdt = OracleType.GetObjectType(tableUdt.ItemObjectType.Name, db);
                    var dbList = new OracleTable(tableUdt);
                    //and the internal list item objects

                    var subPropInfos = dtoPropInfo.PropertyType.GenericTypeArguments[0].GetProperties().ToDictionary(i=>i.Name.ToUpper(), i=>i);
                    //for every item passed in...
                    foreach (var dtoSubItem in listProperty) {
                        //create db objects for every row of data we want to send
                        var dbObj = new OracleObject(itemUdt);

                        //and map the properties from the inbound dto sub items to oracle items by name
                        //using reflection to enumerate the properties by name
                        foreach (OracleAttribute field in itemUdt.Attributes)
                        {
                            var val = subPropInfos[field.Name.ToUpper()].GetValue(dtoSubItem);
                            //small tweak to map inbound booleans to 1's & 0's on the db since oracle doesn't support boolean!?!
                            var isDbBool = field.DbType == OracleDbType.Integer && field.Precision == 1;
                            dbObj[field] = isDbBool ? ((bool)val ? 1 : 0) : val;
                        }

                        //lastly add the db obj to the db table
                        dbList.Add(dbObj);
                    }
                    parm.Value = dbList;
                }
                else {
                    parm.Value = dtoPropInfo.GetValue(dto);
                }
            }

            cmd.ExecuteNonQuery();
        }
    }
}