com.backnd.database 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Attributes/ColumnAttribute.cs +46 -0
- package/Attributes/ColumnAttribute.cs.meta +11 -0
- package/Attributes/PrimaryKeyAttribute.cs +12 -0
- package/Attributes/PrimaryKeyAttribute.cs.meta +11 -0
- package/Attributes/TableAttribute.cs +44 -0
- package/Attributes/TableAttribute.cs.meta +11 -0
- package/Attributes.meta +8 -0
- package/BACKND.Database.asmdef +14 -0
- package/BACKND.Database.asmdef.meta +7 -0
- package/BaseModel.cs +22 -0
- package/BaseModel.cs.meta +11 -0
- package/Client.cs +490 -0
- package/Client.cs.meta +11 -0
- package/Editor/BACKND.Database.Editor.asmdef +18 -0
- package/Editor/BACKND.Database.Editor.asmdef.meta +7 -0
- package/Editor/PackageAssetInstaller.cs +251 -0
- package/Editor/PackageAssetInstaller.cs.meta +11 -0
- package/Editor.meta +8 -0
- package/Exceptions/DatabaseException.cs +34 -0
- package/Exceptions/DatabaseException.cs.meta +11 -0
- package/Exceptions.meta +8 -0
- package/Internal/ExpressionAnalyzer.cs +433 -0
- package/Internal/ExpressionAnalyzer.cs.meta +11 -0
- package/Internal/QueryTypes.cs +61 -0
- package/Internal/QueryTypes.cs.meta +11 -0
- package/Internal/SqlBuilder.cs +375 -0
- package/Internal/SqlBuilder.cs.meta +11 -0
- package/Internal/ValueFormatter.cs +103 -0
- package/Internal/ValueFormatter.cs.meta +11 -0
- package/Internal.meta +8 -0
- package/Network/DatabaseExecutor.cs +171 -0
- package/Network/DatabaseExecutor.cs.meta +11 -0
- package/Network/DatabaseRequest.cs +16 -0
- package/Network/DatabaseRequest.cs.meta +11 -0
- package/Network/DatabaseResponse.cs +81 -0
- package/Network/DatabaseResponse.cs.meta +11 -0
- package/Network.meta +8 -0
- package/QueryBuilder.cs +1001 -0
- package/QueryBuilder.cs.meta +11 -0
- package/README.md +24 -0
- package/TheBackend~/Plugins/Android/Backend.aar +0 -0
- package/TheBackend~/Plugins/Backend.dll +0 -0
- package/TheBackend~/Plugins/Editor/TheBackendMultiSettingEditor.dll +0 -0
- package/TheBackend~/Plugins/Editor/TheBackendSettingEditor.dll +0 -0
- package/TheBackend~/Plugins/LitJSON.dll +0 -0
- package/TheBackend~/Plugins/Settings/TheBackendHashKeySettings.dll +0 -0
- package/TheBackend~/Plugins/Settings/TheBackendMultiSettings.dll +0 -0
- package/TheBackend~/Plugins/Settings/TheBackendSettings.dll +0 -0
- package/Tools/BTask.cs +905 -0
- package/Tools/BTask.cs.meta +11 -0
- package/Tools/DatabaseLoop.cs +110 -0
- package/Tools/DatabaseLoop.cs.meta +11 -0
- package/Tools/JsonHelper.cs +82 -0
- package/Tools/JsonHelper.cs.meta +11 -0
- package/Tools.meta +8 -0
- package/TransactionBuilder.cs +154 -0
- package/TransactionBuilder.cs.meta +11 -0
- package/TransactionQueryBuilder.cs +205 -0
- package/TransactionQueryBuilder.cs.meta +11 -0
- package/TransactionResult.cs +33 -0
- package/TransactionResult.cs.meta +11 -0
- package/package.json +20 -0
- package/package.json.meta +7 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
|
|
3
|
+
using UnityEngine;
|
|
4
|
+
using UnityEngine.LowLevel;
|
|
5
|
+
using UnityEngine.PlayerLoop;
|
|
6
|
+
|
|
7
|
+
namespace BACKND.Database
|
|
8
|
+
{
|
|
9
|
+
public static class DatabaseLoop
|
|
10
|
+
{
|
|
11
|
+
internal enum AddMode { Beginning, End }
|
|
12
|
+
|
|
13
|
+
public static Action OnEarlyUpdate;
|
|
14
|
+
public static Action OnLateUpdate;
|
|
15
|
+
|
|
16
|
+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
|
17
|
+
static void ResetStatics()
|
|
18
|
+
{
|
|
19
|
+
OnEarlyUpdate = null;
|
|
20
|
+
OnLateUpdate = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction function, PlayerLoopSystem playerLoop, Type playerLoopSystemType)
|
|
24
|
+
{
|
|
25
|
+
if (playerLoop.type == playerLoopSystemType)
|
|
26
|
+
return Array.FindIndex(playerLoop.subSystemList, (elem => elem.updateDelegate == function));
|
|
27
|
+
|
|
28
|
+
if (playerLoop.subSystemList != null)
|
|
29
|
+
{
|
|
30
|
+
for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
|
|
31
|
+
{
|
|
32
|
+
int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType);
|
|
33
|
+
if (index != -1) return index;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, Type ownerType, ref PlayerLoopSystem playerLoop, Type playerLoopSystemType, AddMode addMode)
|
|
40
|
+
{
|
|
41
|
+
if (playerLoop.type == playerLoopSystemType)
|
|
42
|
+
{
|
|
43
|
+
if (Array.FindIndex(playerLoop.subSystemList, (s => s.updateDelegate == function)) != -1)
|
|
44
|
+
{
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0;
|
|
49
|
+
Array.Resize(ref playerLoop.subSystemList, oldListLength + 1);
|
|
50
|
+
|
|
51
|
+
PlayerLoopSystem system = new PlayerLoopSystem
|
|
52
|
+
{
|
|
53
|
+
type = ownerType,
|
|
54
|
+
updateDelegate = function
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (addMode == AddMode.Beginning)
|
|
58
|
+
{
|
|
59
|
+
Array.Copy(playerLoop.subSystemList, 0, playerLoop.subSystemList, 1, playerLoop.subSystemList.Length - 1);
|
|
60
|
+
playerLoop.subSystemList[0] = system;
|
|
61
|
+
}
|
|
62
|
+
else if (addMode == AddMode.End)
|
|
63
|
+
{
|
|
64
|
+
playerLoop.subSystemList[oldListLength] = system;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (playerLoop.subSystemList != null)
|
|
71
|
+
{
|
|
72
|
+
for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
|
|
73
|
+
{
|
|
74
|
+
if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
|
82
|
+
static void RuntimeInitializeOnLoad()
|
|
83
|
+
{
|
|
84
|
+
PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop();
|
|
85
|
+
|
|
86
|
+
bool earlyAdded = AddToPlayerLoop(DatabaseEarlyUpdate, typeof(DatabaseLoop), ref playerLoop, typeof(EarlyUpdate), AddMode.End);
|
|
87
|
+
bool lateAdded = AddToPlayerLoop(DatabaseLateUpdate, typeof(DatabaseLoop), ref playerLoop, typeof(PreLateUpdate), AddMode.End);
|
|
88
|
+
|
|
89
|
+
PlayerLoop.SetPlayerLoop(playerLoop);
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static void DatabaseEarlyUpdate()
|
|
94
|
+
{
|
|
95
|
+
if (!Application.isPlaying) return;
|
|
96
|
+
|
|
97
|
+
OnEarlyUpdate?.Invoke();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static void DatabaseLateUpdate()
|
|
101
|
+
{
|
|
102
|
+
if (!Application.isPlaying) return;
|
|
103
|
+
|
|
104
|
+
if (OnLateUpdate != null)
|
|
105
|
+
{
|
|
106
|
+
OnLateUpdate.Invoke();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Globalization;
|
|
3
|
+
|
|
4
|
+
using Newtonsoft.Json;
|
|
5
|
+
|
|
6
|
+
namespace BACKND.Database
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// JSON serialization helper for database operations
|
|
10
|
+
/// </summary>
|
|
11
|
+
public static class JsonHelper
|
|
12
|
+
{
|
|
13
|
+
/// <summary>
|
|
14
|
+
/// Deserialize object from JSON string to target type
|
|
15
|
+
/// </summary>
|
|
16
|
+
/// <typeparam name="T">Target type</typeparam>
|
|
17
|
+
/// <param name="value">Input object (string or other)</param>
|
|
18
|
+
/// <returns>Deserialized object of type T</returns>
|
|
19
|
+
public static T DeserializeObject<T>(object value)
|
|
20
|
+
{
|
|
21
|
+
if (value == null)
|
|
22
|
+
return default(T);
|
|
23
|
+
|
|
24
|
+
// If the value is already the correct type, return it directly
|
|
25
|
+
if (value is T directValue)
|
|
26
|
+
return directValue;
|
|
27
|
+
|
|
28
|
+
// Convert to string and deserialize
|
|
29
|
+
string jsonString = value.ToString();
|
|
30
|
+
|
|
31
|
+
try
|
|
32
|
+
{
|
|
33
|
+
return JsonConvert.DeserializeObject<T>(jsonString);
|
|
34
|
+
}
|
|
35
|
+
catch (Exception ex)
|
|
36
|
+
{
|
|
37
|
+
UnityEngine.Debug.LogWarning($"Failed to deserialize JSON for type {typeof(T).Name}: {ex.Message}");
|
|
38
|
+
return default(T);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// <summary>
|
|
43
|
+
/// Serialize object to JSON string
|
|
44
|
+
/// </summary>
|
|
45
|
+
/// <param name="value">Object to serialize</param>
|
|
46
|
+
/// <returns>JSON string</returns>
|
|
47
|
+
public static string SerializeObject(object value)
|
|
48
|
+
{
|
|
49
|
+
if (value == null)
|
|
50
|
+
return null;
|
|
51
|
+
|
|
52
|
+
try
|
|
53
|
+
{
|
|
54
|
+
return JsonConvert.SerializeObject(value);
|
|
55
|
+
}
|
|
56
|
+
catch (Exception ex)
|
|
57
|
+
{
|
|
58
|
+
UnityEngine.Debug.LogWarning($"Failed to serialize object to JSON: {ex.Message}");
|
|
59
|
+
return value.ToString();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// <summary>
|
|
64
|
+
/// Convert object to string, preserving ISO 8601 format for DateTime
|
|
65
|
+
/// </summary>
|
|
66
|
+
/// <param name="value">Input object</param>
|
|
67
|
+
/// <returns>String representation</returns>
|
|
68
|
+
public static string ConvertToString(object value)
|
|
69
|
+
{
|
|
70
|
+
if (value == null)
|
|
71
|
+
return null;
|
|
72
|
+
|
|
73
|
+
if (value is string strValue)
|
|
74
|
+
return strValue;
|
|
75
|
+
|
|
76
|
+
if (value is DateTime dateValue)
|
|
77
|
+
return dateValue.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
|
|
78
|
+
|
|
79
|
+
return value.ToString();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
package/Tools.meta
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Text;
|
|
4
|
+
|
|
5
|
+
using BACKND.Database.Network;
|
|
6
|
+
|
|
7
|
+
using Newtonsoft.Json;
|
|
8
|
+
|
|
9
|
+
namespace BACKND.Database
|
|
10
|
+
{
|
|
11
|
+
/// <summary>
|
|
12
|
+
/// 트랜잭션 빌더 - 여러 데이터베이스 작업을 원자적으로 실행
|
|
13
|
+
/// </summary>
|
|
14
|
+
public class TransactionBuilder
|
|
15
|
+
{
|
|
16
|
+
private readonly Client client;
|
|
17
|
+
private readonly List<string> statements = new();
|
|
18
|
+
|
|
19
|
+
/// <summary>
|
|
20
|
+
/// 현재 활성화된 TransactionQueryBuilder (암묵적 실행용)
|
|
21
|
+
/// </summary>
|
|
22
|
+
internal TransactionQueryBuilderBase activeQueryBuilder;
|
|
23
|
+
|
|
24
|
+
internal TransactionBuilder(Client client)
|
|
25
|
+
{
|
|
26
|
+
this.client = client;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// <summary>
|
|
30
|
+
/// 특정 모델 타입에 대한 쿼리 빌더 시작
|
|
31
|
+
/// </summary>
|
|
32
|
+
public TransactionQueryBuilder<T> From<T>() where T : BaseModel, new()
|
|
33
|
+
{
|
|
34
|
+
// 이전에 활성화된 QueryBuilder가 있고 미완료 작업이 있으면 flush
|
|
35
|
+
FlushActiveQueryBuilder();
|
|
36
|
+
|
|
37
|
+
var queryBuilder = new TransactionQueryBuilder<T>(this, client);
|
|
38
|
+
activeQueryBuilder = queryBuilder;
|
|
39
|
+
return queryBuilder;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// <summary>
|
|
43
|
+
/// 활성화된 QueryBuilder의 미완료 작업을 flush
|
|
44
|
+
/// </summary>
|
|
45
|
+
internal void FlushActiveQueryBuilder()
|
|
46
|
+
{
|
|
47
|
+
if (activeQueryBuilder != null && activeQueryBuilder.HasPendingSetClauses)
|
|
48
|
+
{
|
|
49
|
+
activeQueryBuilder.FlushPendingSetClauses();
|
|
50
|
+
}
|
|
51
|
+
activeQueryBuilder = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// <summary>
|
|
55
|
+
/// SQL 문장 추가 (내부용)
|
|
56
|
+
/// </summary>
|
|
57
|
+
internal void AddStatement(string sql)
|
|
58
|
+
{
|
|
59
|
+
if (string.IsNullOrWhiteSpace(sql))
|
|
60
|
+
throw new ArgumentException("SQL statement cannot be empty");
|
|
61
|
+
|
|
62
|
+
statements.Add(sql);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// <summary>
|
|
66
|
+
/// 현재 트랜잭션에 포함된 작업 수
|
|
67
|
+
/// </summary>
|
|
68
|
+
public int Count => statements.Count;
|
|
69
|
+
|
|
70
|
+
/// <summary>
|
|
71
|
+
/// 트랜잭션 실행
|
|
72
|
+
/// </summary>
|
|
73
|
+
public async BTask<TransactionResult> CommitAsync()
|
|
74
|
+
{
|
|
75
|
+
// 미완료 작업이 있으면 flush
|
|
76
|
+
FlushActiveQueryBuilder();
|
|
77
|
+
|
|
78
|
+
if (statements.Count == 0)
|
|
79
|
+
throw new InvalidOperationException("Transaction has no operations. Add at least one operation before committing.");
|
|
80
|
+
|
|
81
|
+
// DynamoDB TransactWriteItems 제한: 100개
|
|
82
|
+
const int maxOperations = 100;
|
|
83
|
+
if (statements.Count > maxOperations)
|
|
84
|
+
throw new InvalidOperationException($"Transaction exceeds maximum operations limit. Maximum: {maxOperations}, Current: {statements.Count}");
|
|
85
|
+
|
|
86
|
+
// 트랜잭션 SQL 생성
|
|
87
|
+
var sb = new StringBuilder();
|
|
88
|
+
sb.AppendLine("BEGIN;");
|
|
89
|
+
foreach (var statement in statements)
|
|
90
|
+
{
|
|
91
|
+
sb.Append(statement);
|
|
92
|
+
if (!statement.EndsWith(";"))
|
|
93
|
+
sb.Append(";");
|
|
94
|
+
sb.AppendLine();
|
|
95
|
+
}
|
|
96
|
+
sb.Append("COMMIT;");
|
|
97
|
+
|
|
98
|
+
var request = new DatabaseRequest
|
|
99
|
+
{
|
|
100
|
+
Query = sb.ToString(),
|
|
101
|
+
Parameters = new Dictionary<string, object>()
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// user_uuid 파라미터 추가
|
|
105
|
+
if (client.UserUUID != null)
|
|
106
|
+
{
|
|
107
|
+
request.Parameters["@current_user_uuid"] = client.UserUUID;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var response = await client.ExecuteTransaction(request);
|
|
111
|
+
|
|
112
|
+
if (!response.Success)
|
|
113
|
+
{
|
|
114
|
+
return new TransactionResult
|
|
115
|
+
{
|
|
116
|
+
Success = false,
|
|
117
|
+
OperationCount = statements.Count,
|
|
118
|
+
Error = response.Error,
|
|
119
|
+
Message = "Transaction failed"
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 결과 파싱 시도
|
|
124
|
+
TransactionResult result;
|
|
125
|
+
try
|
|
126
|
+
{
|
|
127
|
+
result = JsonConvert.DeserializeObject<TransactionResult>(response.Result);
|
|
128
|
+
if (result == null)
|
|
129
|
+
{
|
|
130
|
+
result = new TransactionResult();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch
|
|
134
|
+
{
|
|
135
|
+
result = new TransactionResult();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
result.Success = true;
|
|
139
|
+
result.OperationCount = statements.Count;
|
|
140
|
+
result.Message = result.Message ?? "Transaction committed successfully";
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// <summary>
|
|
147
|
+
/// TransactionQueryBuilder 베이스 클래스 (암묵적 실행 지원용)
|
|
148
|
+
/// </summary>
|
|
149
|
+
public abstract class TransactionQueryBuilderBase
|
|
150
|
+
{
|
|
151
|
+
internal abstract bool HasPendingSetClauses { get; }
|
|
152
|
+
internal abstract void FlushPendingSetClauses();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Linq.Expressions;
|
|
5
|
+
|
|
6
|
+
using BACKND.Database.Internal;
|
|
7
|
+
|
|
8
|
+
namespace BACKND.Database
|
|
9
|
+
{
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// 트랜잭션용 쿼리 빌더
|
|
12
|
+
/// </summary>
|
|
13
|
+
public class TransactionQueryBuilder<T> : TransactionQueryBuilderBase where T : BaseModel, new()
|
|
14
|
+
{
|
|
15
|
+
private readonly TransactionBuilder transaction;
|
|
16
|
+
private readonly Client client;
|
|
17
|
+
private readonly T modelInstance;
|
|
18
|
+
private readonly ExpressionAnalyzer expressionAnalyzer;
|
|
19
|
+
|
|
20
|
+
private readonly List<WhereCondition> whereConditions = new();
|
|
21
|
+
private readonly List<SetClause> setClauses = new();
|
|
22
|
+
private bool isOfCurrentUser;
|
|
23
|
+
|
|
24
|
+
internal TransactionQueryBuilder(TransactionBuilder transaction, Client client)
|
|
25
|
+
{
|
|
26
|
+
this.transaction = transaction;
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.modelInstance = new T();
|
|
29
|
+
this.expressionAnalyzer = new ExpressionAnalyzer(modelInstance);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
internal override bool HasPendingSetClauses => setClauses.Count > 0;
|
|
33
|
+
|
|
34
|
+
internal override void FlushPendingSetClauses()
|
|
35
|
+
{
|
|
36
|
+
if (setClauses.Count == 0)
|
|
37
|
+
return;
|
|
38
|
+
|
|
39
|
+
if (whereConditions.Count == 0)
|
|
40
|
+
throw new InvalidOperationException("Inc/Dec requires a WHERE clause");
|
|
41
|
+
|
|
42
|
+
var whereClause = SqlBuilder.BuildWhereClause(
|
|
43
|
+
whereConditions,
|
|
44
|
+
isOfCurrentUser,
|
|
45
|
+
modelInstance.GetTableType());
|
|
46
|
+
var sql = SqlBuilder.BuildUpdateQueryFromSetClauses(
|
|
47
|
+
modelInstance.GetTableName(),
|
|
48
|
+
setClauses,
|
|
49
|
+
whereClause);
|
|
50
|
+
|
|
51
|
+
transaction.AddStatement(sql);
|
|
52
|
+
setClauses.Clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#region 조건 빌더 (체이닝 → 자신 반환)
|
|
56
|
+
|
|
57
|
+
/// <summary>
|
|
58
|
+
/// WHERE 조건 추가
|
|
59
|
+
/// </summary>
|
|
60
|
+
public TransactionQueryBuilder<T> Where(Expression<Func<T, bool>> predicate)
|
|
61
|
+
{
|
|
62
|
+
var condition = expressionAnalyzer.AnalyzeSingle(predicate, whereConditions);
|
|
63
|
+
if (condition != null)
|
|
64
|
+
{
|
|
65
|
+
whereConditions.Add(condition);
|
|
66
|
+
}
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// <summary>
|
|
71
|
+
/// 현재 사용자 데이터만 대상 (UserTable용)
|
|
72
|
+
/// </summary>
|
|
73
|
+
public TransactionQueryBuilder<T> OfCurrentUser()
|
|
74
|
+
{
|
|
75
|
+
isOfCurrentUser = true;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// <summary>
|
|
80
|
+
/// 필드 값 증가
|
|
81
|
+
/// </summary>
|
|
82
|
+
public TransactionQueryBuilder<T> Inc<TField>(Expression<Func<T, TField>> selector, TField value)
|
|
83
|
+
{
|
|
84
|
+
var columnName = expressionAnalyzer.GetColumnNameFromKeySelector(selector);
|
|
85
|
+
ValidateNoDuplicateSetClause(columnName);
|
|
86
|
+
setClauses.Add(new SetClause
|
|
87
|
+
{
|
|
88
|
+
ColumnName = columnName,
|
|
89
|
+
Operator = "+",
|
|
90
|
+
Value = value
|
|
91
|
+
});
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// <summary>
|
|
96
|
+
/// 필드 값 감소
|
|
97
|
+
/// </summary>
|
|
98
|
+
public TransactionQueryBuilder<T> Dec<TField>(Expression<Func<T, TField>> selector, TField value)
|
|
99
|
+
{
|
|
100
|
+
var columnName = expressionAnalyzer.GetColumnNameFromKeySelector(selector);
|
|
101
|
+
ValidateNoDuplicateSetClause(columnName);
|
|
102
|
+
setClauses.Add(new SetClause
|
|
103
|
+
{
|
|
104
|
+
ColumnName = columnName,
|
|
105
|
+
Operator = "-",
|
|
106
|
+
Value = value
|
|
107
|
+
});
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private void ValidateNoDuplicateSetClause(string columnName)
|
|
112
|
+
{
|
|
113
|
+
if (setClauses.Any(c => c.ColumnName == columnName))
|
|
114
|
+
throw new InvalidOperationException($"'{columnName}' is already being modified. Each field can only be modified once.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#endregion
|
|
118
|
+
|
|
119
|
+
#region 실행 메서드 (SQL 추가 → TransactionBuilder 반환)
|
|
120
|
+
|
|
121
|
+
/// <summary>
|
|
122
|
+
/// INSERT 작업 추가
|
|
123
|
+
/// </summary>
|
|
124
|
+
public TransactionBuilder Insert(T model)
|
|
125
|
+
{
|
|
126
|
+
if (model == null)
|
|
127
|
+
throw new ArgumentNullException(nameof(model));
|
|
128
|
+
|
|
129
|
+
var sql = SqlBuilder.BuildInsertQuery(model, out _);
|
|
130
|
+
transaction.AddStatement(sql);
|
|
131
|
+
|
|
132
|
+
// 활성 QueryBuilder 해제
|
|
133
|
+
transaction.activeQueryBuilder = null;
|
|
134
|
+
return transaction;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// <summary>
|
|
138
|
+
/// UPDATE 작업 추가
|
|
139
|
+
/// </summary>
|
|
140
|
+
public TransactionBuilder Update(T model)
|
|
141
|
+
{
|
|
142
|
+
if (model == null)
|
|
143
|
+
throw new ArgumentNullException(nameof(model));
|
|
144
|
+
|
|
145
|
+
// WHERE 절이 없으면 PrimaryKey로 자동 생성
|
|
146
|
+
if (whereConditions.Count == 0)
|
|
147
|
+
{
|
|
148
|
+
var pkConditions = SqlBuilder.BuildPrimaryKeyConditions(model);
|
|
149
|
+
whereConditions.AddRange(pkConditions);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var whereClause = SqlBuilder.BuildWhereClause(
|
|
153
|
+
whereConditions,
|
|
154
|
+
isOfCurrentUser,
|
|
155
|
+
modelInstance.GetTableType());
|
|
156
|
+
var sql = SqlBuilder.BuildUpdateQuery(model, whereClause, out _);
|
|
157
|
+
transaction.AddStatement(sql);
|
|
158
|
+
|
|
159
|
+
// 활성 QueryBuilder 해제
|
|
160
|
+
transaction.activeQueryBuilder = null;
|
|
161
|
+
return transaction;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// <summary>
|
|
165
|
+
/// DELETE 작업 추가
|
|
166
|
+
/// </summary>
|
|
167
|
+
public TransactionBuilder Delete()
|
|
168
|
+
{
|
|
169
|
+
if (whereConditions.Count == 0)
|
|
170
|
+
throw new InvalidOperationException("Delete requires a WHERE clause");
|
|
171
|
+
|
|
172
|
+
var whereClause = SqlBuilder.BuildWhereClause(
|
|
173
|
+
whereConditions,
|
|
174
|
+
isOfCurrentUser,
|
|
175
|
+
modelInstance.GetTableType());
|
|
176
|
+
var sql = SqlBuilder.BuildDeleteQuery(modelInstance.GetTableName(), whereClause);
|
|
177
|
+
transaction.AddStatement(sql);
|
|
178
|
+
|
|
179
|
+
// 활성 QueryBuilder 해제
|
|
180
|
+
transaction.activeQueryBuilder = null;
|
|
181
|
+
return transaction;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#endregion
|
|
185
|
+
|
|
186
|
+
#region 암묵적 실행을 위한 From<U>()
|
|
187
|
+
|
|
188
|
+
/// <summary>
|
|
189
|
+
/// 다른 테이블로 전환 (현재 Inc/Dec 작업이 있으면 자동 flush)
|
|
190
|
+
/// </summary>
|
|
191
|
+
public TransactionQueryBuilder<U> From<U>() where U : BaseModel, new()
|
|
192
|
+
{
|
|
193
|
+
// 현재 Inc/Dec 작업이 있으면 자동으로 UPDATE SQL 생성
|
|
194
|
+
if (HasPendingSetClauses)
|
|
195
|
+
{
|
|
196
|
+
FlushPendingSetClauses();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 새 QueryBuilder 생성
|
|
200
|
+
return transaction.From<U>();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#endregion
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
namespace BACKND.Database
|
|
2
|
+
{
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// 트랜잭션 실행 결과
|
|
5
|
+
/// </summary>
|
|
6
|
+
public class TransactionResult
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// 트랜잭션 성공 여부
|
|
10
|
+
/// </summary>
|
|
11
|
+
public bool Success { get; set; }
|
|
12
|
+
|
|
13
|
+
/// <summary>
|
|
14
|
+
/// 트랜잭션에 포함된 작업 수
|
|
15
|
+
/// </summary>
|
|
16
|
+
public int OperationCount { get; set; }
|
|
17
|
+
|
|
18
|
+
/// <summary>
|
|
19
|
+
/// 결과 메시지
|
|
20
|
+
/// </summary>
|
|
21
|
+
public string Message { get; set; }
|
|
22
|
+
|
|
23
|
+
/// <summary>
|
|
24
|
+
/// 에러 메시지 (실패 시)
|
|
25
|
+
/// </summary>
|
|
26
|
+
public string Error { get; set; }
|
|
27
|
+
|
|
28
|
+
/// <summary>
|
|
29
|
+
/// 영향받은 총 행 수
|
|
30
|
+
/// </summary>
|
|
31
|
+
public int TotalAffectedRows { get; set; }
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "com.backnd.database",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"displayName": "BACKND Database",
|
|
5
|
+
"description": "BACKND Database is a Unity SDK for seamless integration with BACKND cloud database services.\n\nEasily manage and synchronize game data such as player profiles, game states, and leaderboards across multiple platforms.\nIdeal for Unity developers looking to implement robust database solutions without complex backend setups.",
|
|
6
|
+
"unity": "2021.3",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"backnd",
|
|
9
|
+
"unity",
|
|
10
|
+
"database",
|
|
11
|
+
"networking"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {},
|
|
14
|
+
"category": "SDK",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "backnd",
|
|
17
|
+
"email": "help@thebackend.io",
|
|
18
|
+
"url": "https://backnd.com"
|
|
19
|
+
}
|
|
20
|
+
}
|