Sync README files from source repository [skip ci]
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
# SmartPool - Dapper + ClickHouse.Driver Integration
|
||||
|
||||
## Overview
|
||||
|
||||
**Dapper WORKS with ClickHouse.Driver**, but with specific requirements for parameter binding.
|
||||
|
||||
## ✅ Correct Usage
|
||||
|
||||
### Dictionary Parameters (Required)
|
||||
|
||||
ClickHouse.Driver requires parameters to be passed as `Dictionary<string, object>`:
|
||||
|
||||
```csharp
|
||||
using Dapper;
|
||||
using ClickHouse.Driver.ADO;
|
||||
|
||||
// ✅ CORRECT: Use Dictionary<string, object>
|
||||
var sql = "SELECT * FROM table WHERE id = {id:Int32} AND name = {name:String}";
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "id", 42 },
|
||||
{ "name", "test" }
|
||||
};
|
||||
|
||||
using var connection = new ClickHouseConnection(connectionString);
|
||||
var result = await connection.QueryAsync<MyClass>(sql, parameters);
|
||||
```
|
||||
|
||||
### Parameter Placeholder Syntax
|
||||
|
||||
ClickHouse uses `{paramName:Type}` syntax:
|
||||
|
||||
| Type | Placeholder | Example |
|
||||
|------|-------------|---------|
|
||||
| String | `{name:String}` | `WHERE name = {name:String}` |
|
||||
| Int32 | `{id:Int32}` | `WHERE id = {id:Int32}` |
|
||||
| Int64 | `{id:Int64}` | `WHERE id = {id:Int64}` |
|
||||
| Float64 | `{price:Float64}` | `WHERE price > {price:Float64}` |
|
||||
| DateTime | `{dt:DateTime}` | `WHERE created_at > {dt:DateTime}` |
|
||||
|
||||
## ❌ NOT Supported
|
||||
|
||||
### Anonymous Objects
|
||||
|
||||
```csharp
|
||||
// ❌ WRONG: Anonymous objects do NOT work with ClickHouse.Driver
|
||||
var result = await connection.QueryAsync<MyClass>(
|
||||
"SELECT * FROM table WHERE id = {id:Int32}",
|
||||
new { id = 42 } // NOT SUPPORTED
|
||||
);
|
||||
```
|
||||
|
||||
## Real Examples from ProxyMetadataRepository
|
||||
|
||||
### SELECT Query
|
||||
|
||||
```csharp
|
||||
public async Task<List<SmartProxyServer>> GetProxiesByAccessTokenAsync(
|
||||
string accessToken,
|
||||
string? ipVersion = null,
|
||||
string? protocol = null,
|
||||
string? country = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = @"
|
||||
SELECT DISTINCT
|
||||
m.id AS Id,
|
||||
m.host AS Host,
|
||||
m.port AS Port,
|
||||
m.protocol AS Protocol,
|
||||
m.ip_version AS IpVersion,
|
||||
m.country AS Country
|
||||
FROM smart_pool_meta.proxy_metadata FINAL m
|
||||
INNER JOIN smart_pool_meta.proxy_access_mapping FINAL a
|
||||
ON m.cluster = a.cluster
|
||||
WHERE a.access_token = {access_token:String}
|
||||
AND m.status = 1";
|
||||
|
||||
// Build parameters dictionary
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "access_token", accessToken }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(ipVersion))
|
||||
{
|
||||
sql += " AND m.ip_version = {ip_version:String}";
|
||||
parameters["ip_version"] = ipVersion;
|
||||
}
|
||||
|
||||
using var connection = _clickHouseContext.smart_pool_meta;
|
||||
var result = await connection.QueryAsync<SmartProxyServer>(sql, parameters);
|
||||
return result.ToList();
|
||||
}
|
||||
```
|
||||
|
||||
### SELECT Single Row
|
||||
|
||||
```csharp
|
||||
public async Task<SmartProxyServer?> GetProxyByIdAsync(
|
||||
int proxyId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = @"
|
||||
SELECT
|
||||
id AS Id,
|
||||
host AS Host,
|
||||
port AS Port
|
||||
FROM smart_pool_meta.proxy_metadata FINAL
|
||||
WHERE id = {proxy_id:Int32} AND status = 1
|
||||
LIMIT 1";
|
||||
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "proxy_id", proxyId }
|
||||
};
|
||||
|
||||
using var connection = _clickHouseContext.smart_pool_meta;
|
||||
return await connection.QueryFirstOrDefaultAsync<SmartProxyServer>(sql, parameters);
|
||||
}
|
||||
```
|
||||
|
||||
### INSERT Query
|
||||
|
||||
```csharp
|
||||
public async Task<bool> UpsertProxyAsync(
|
||||
SmartProxyServer proxy,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = @"
|
||||
INSERT INTO smart_pool_meta.proxy_metadata
|
||||
(id, host, port, protocol, ip_version, country, status, updated_at)
|
||||
VALUES
|
||||
({id:Int32}, {host:String}, {port:Int32}, {protocol:String},
|
||||
{ip_version:String}, {country:String}, {status:Int32}, now64(3))";
|
||||
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "id", proxy.Id },
|
||||
{ "host", proxy.Host },
|
||||
{ "port", proxy.Port },
|
||||
{ "protocol", proxy.Protocol },
|
||||
{ "ip_version", proxy.IpVersion },
|
||||
{ "country", proxy.Country ?? "" },
|
||||
{ "status", proxy.Status }
|
||||
};
|
||||
|
||||
using var connection = _clickHouseContext.smart_pool_meta;
|
||||
var result = await connection.ExecuteAsync(sql, parameters);
|
||||
return result > 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### ✅ Advantages
|
||||
|
||||
1. **Type Safety**: ClickHouse type system enforced at SQL level
|
||||
2. **SQL Injection Prevention**: Parameters are properly escaped
|
||||
3. **Clean Code**: No manual string escaping needed
|
||||
4. **Performance**: Efficient parameter binding
|
||||
5. **Object Mapping**: Automatic mapping to POCOs
|
||||
|
||||
### 🔧 Trade-offs
|
||||
|
||||
1. **Dictionary Only**: Must use `Dictionary<string, object>`, not anonymous objects
|
||||
2. **Type Annotations**: Must specify ClickHouse types in placeholders
|
||||
3. **Manual Dictionary Building**: More verbose than anonymous objects
|
||||
|
||||
## Dapper Methods Supported
|
||||
|
||||
| Method | Purpose | Example |
|
||||
|--------|---------|---------|
|
||||
| `QueryAsync<T>` | SELECT returning multiple rows | `connection.QueryAsync<SmartProxyServer>(sql, params)` |
|
||||
| `QueryFirstOrDefaultAsync<T>` | SELECT returning 0 or 1 row | `connection.QueryFirstOrDefaultAsync<SmartProxyServer>(sql, params)` |
|
||||
| `ExecuteAsync` | INSERT/UPDATE/DELETE | `connection.ExecuteAsync(sql, params)` |
|
||||
| `QuerySingleAsync<T>` | SELECT returning exactly 1 row | `connection.QuerySingleAsync<int>(sql, params)` |
|
||||
|
||||
## Migration Summary
|
||||
|
||||
### Before (String Replacement)
|
||||
|
||||
```csharp
|
||||
var sql = $@"
|
||||
SELECT * FROM table
|
||||
WHERE id = {proxyId}
|
||||
AND name = '{EscapeString(name)}'";
|
||||
|
||||
using var connection = _clickHouseContext.db;
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
// Manual object mapping
|
||||
var obj = new MyClass
|
||||
{
|
||||
Id = reader.GetInt32(0),
|
||||
Name = reader.GetString(1)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### After (Dapper + Dictionary)
|
||||
|
||||
```csharp
|
||||
var sql = @"
|
||||
SELECT * FROM table
|
||||
WHERE id = {id:Int32}
|
||||
AND name = {name:String}";
|
||||
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "id", proxyId },
|
||||
{ "name", name }
|
||||
};
|
||||
|
||||
using var connection = _clickHouseContext.db;
|
||||
var result = await connection.QueryAsync<MyClass>(sql, parameters);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use Dictionary parameters** - Never inline values
|
||||
2. **Specify ClickHouse types** - Use proper type annotations
|
||||
3. **Handle nulls** - Convert null to empty string or default value
|
||||
4. **Use FINAL modifier** - For ClickHouse tables with updates
|
||||
5. **Map to POCOs** - Let Dapper handle object mapping
|
||||
|
||||
## References
|
||||
|
||||
- ClickHouse.Driver: https://www.nuget.org/packages/ClickHouse.Driver
|
||||
- Dapper: https://github.com/DapperLib/Dapper
|
||||
- ClickHouse C# Integration: https://clickhouse.com/docs/integrations/csharp
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Production Ready
|
||||
|
||||
**Date:** 2026-01-22
|
||||
|
||||
**Version:** ClickHouse.Driver 0.9.0 + Dapper 2.1.35
|
||||
Reference in New Issue
Block a user