Я работаю над крошечной F # ADO. NET "оболочкой" (да, еще одной, помимо Npg Зайда Аджаджа sql .FSharp , Дональда Пима Брауэрса и многие другие на GitHub), и я думаю о расширении поддержки для различных ADO. NET провайдеров ...
В основном у меня есть основной проект (ie. Michelle.Sql.Core
), содержащий основные типы + функции, немного похожи на Dapper:
type IDbValue<'DbConnection, 'DbParameter
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter> =
abstract ToParameter: string -> 'DbParameter
type CommandDefinition<'DbConnection, 'DbParameter, 'DbType
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter
and 'DbType :> IDbValue<'DbConnection, 'DbParameter>> =
{ Statement: Statement
Parameters: (string * 'DbType) list
CancellationToken: CancellationToken
Timeout: TimeSpan
StoredProcedure: bool
Prepare: bool
Transaction: DbTransaction option }
Во-первых, вы можете подумать: «Wo sh есть много универсальных шаблонов, украшающих ваши определения типов!».
Хорошо, обо всем по порядку. Я пытаюсь обойти некоторые ограничения, наиболее заметные из которых: https://github.com/fsharp/fslang-suggestions/issues/255 (вместе с его хорошим другом ), подумал Я мог бы обойти эту проблему, создав проект C# и наложив ограничения в этом проекте, это не сработает.
Причина, по которой мне нужно много общих c ограничений, заключается в том, что я хочу сильно- типизированное соединение, которое как бы "протекает" через вызовы, устанавливая значения различных полей этой записи, например:
let playWithSQLite() =
use connection = new SQLiteConnection()
Sql.statement "INSERT INTO aTable (aColumn) VALUES(@aNumber);"
|> Sql.prepare true
|> Sql.timeout (TimeSpan.FromMinutes(1.))
|> Sql.parameters [("aNumber", SqliteDbValue.Integer 42L)]
|> Sql.executeNonQuery connection
Fyi, SqliteDbValue
определяется в другой сборке Michelle.Sql.Sqlite
:
// https://www.sqlite.org/datatype3.html
type SqliteDbValue =
| Null
| Integer of int64
| Real of double
| Text of string
| Blob of byte array
interface IDbValue<SQLiteConnection, SQLiteParameter> with
member this.ToParameter(name) =
let parameter = SQLiteParameter()
// Not so secret impl. goes here...
parameter
Приведенный выше код работает, в основном, запись CommandDefinition
заполняется различными вызовами, определенными в основной библиотеке через модуль Sql
(украшенный RequiredAccessAttribute
).
Проблема возникает, когда при использовании необходимо явно указать общий c возвращаемый тип ...
[<RequireQualifiedAccess>]
module Sql =
// [...]
let executeNonQuery
(connection: 'DbConnection when 'DbConnection :> DbConnection)
(commandDefinition: CommandDefinition<'DbConnection, 'DbParameter, 'DbType>
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter
and 'DbType :> IDbValue<'DbConnection, 'DbParameter>) =
async {
// Not so secret impl. goes here
}
let executeScalar<'Scalar, .. >
(connection: 'DbConnection when 'DbConnection :> DbConnection)
(commandDefinition: CommandDefinition<'DbConnection, 'DbParameter, 'DbType>
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter
and 'DbType :> IDbValue<'DbConnection, 'DbParameter>) =
async {
// Not so secret impl. goes here
}
Итак, вы видите, что в случае функции executeScalar
выше, поскольку один тип должен быть явным, это означает, что каждый другой параметр generi c теперь должен быть явным при вызове этой функции, в противном случае они имеют значение по умолчанию obj
, что, среди прочего, означает, что конечный пользователь теперь должен ввести 4 общих c параметра:
// [...] setting up the CommandDefinition...
|> Sql.executeScalar<int64, SQLiteConnection, SQLiteParameter, SqliteDbValue> connection
, и это именно то, чего я хотел бы избежать при сохранении согласованности соединения.
Что Я пробовал, и это довольно неуклюжее решение - реализовать сокращенную версию executeScalar
, и что я имею в виду под:
module Michelle.Sql.Sqlite
[<RequireQualifiedAccess>]
module Sql =
let executeScalar<'Scalar> connection commandDefinition =
Sql.executeScalar<'Scalar, SQLiteConnection, SQLiteParameter, SqliteDbValue>
connection
commandDefinition
Но суть стратегии в том, что она, по сути, сводится к затенению:
Следовательно, этот код ниже не работает:
open Michelle.Sql.Sqlite
open Michelle.Sql.Core
// [...] setting up the CommandDefinition... connection being an instance of SQLiteConnection
|> Sql.executeScalar<int64> connection
Пока он работает:
open Michelle.Sql.Core
open Michelle.Sql.Sqlite
// [...] setting up the CommandDefinition... connection being an instance of SQLiteConnection
|> Sql.executeScalar<int64> connection
I wi sh может быть решение, я хотя про stati c classe s, но частичные классы не могут быть определены в нескольких сборках.
Я знаю, что перегрузка невозможна с функциями модуля F #, и затенение не похоже на жизнеспособное решение с точки зрения опыта разработчика.
Итак, есть ли какое-нибудь решение? (Не говоря уже о создании другой функции с другим именем или другого модуля с другим именем)