OrientDB-NET.模型二进制
本文关键字:二进制 模型 OrientDB-NET | 更新日期: 2023-09-27 18:28:26
我的团队有兴趣将现有的MySQL数据库(碰巧非常难看,索引也很差)迁移到OrientDB。由于我们的应用程序是用C#编写的,我正在研究我们从C#应用程序中访问数据库的能力。
我发现OrientDB-NET.binary作为一个图书馆,应该可以做我想做的事情,然而,我很难让它做我想要的一切。我正在测试的数据库非常简单,来自OrientDB文档:
create class Person extends V
create class Car extends V
create class Country extends V
create class Owns extends E
create class Lives extends E
create property Owns.out LINK Person
create property Owns.in LINK Car
create property Lives.out LINK Person
create property Lives.in LINK Country
alter property Owns.out MANDATORY=true
alter property Owns.in MANDATORY=true
alter property Lives.out MANDATORY=true
alter property Lives.in MANDATORY=true
create index UniqueOwns on Owns(in) unique
create index UniqueLives on Lives(out) unique
当我在DB中查询ODocuments时,我得到了很多期望的信息:
OClient.CreateDatabasePool("127.0.0.1", 2424, "cars",
ODatabaseType.Graph, "admin", "admin", 10, "Cars");
using (ODatabase db = new ODatabase("Cars"))
{
var results = db.Select().Form("Country").ToList();
foreach (var item in results)
{
foreach(var key in item.Keys)
{
Console.WriteLine(key);
if (item[key] is IEnumerable<ORID>)
{
foreach (var element in item[key] as IEnumerable<ORID>)
{
Console.WriteLine(" " + element);
}
}
else
{
Console.WriteLine(" " + item[key]);
}
}
Console.WriteLine();
}
}
Console.ReadKey();
输出:
@ORID
#15:0
@OVersion
5
@OType
Document
@OClassId
0
@OClassName
Country
Name
UK
in_Lives
#16:0
@ORID
#15:1
@OVersion
3
@OType
Document
@OClassId
0
@OClassName
Country
Name
US
in_Lives
#16:1
#16:2
如果我创建一个类来表示我的顶点,我可以查询它们:
public class Person
{
public string Name { get; set; }
}
public class Country
{
public string Name { get; set; }
}
public class Car
{
public string Name { get; set; }
}
//...
var results = db.Select().From("Country").ToList<Country>();
foreach (var item in results)
{
Console.WriteLine(item.Name);
}
输出:
UK
US
然而,我希望能够访问我的顶点之间的连接:
public class Person
{
public string Name { get; set; }
public Country Lives { get; set; }
public IEnumerable<Car> Owns { get; set; }
}
public class Country
{
public string Name { get; set; }
public IEnumerable<Person> Lives { get; set; }
}
public class Car
{
public string Name { get; set; }
public Person Owns { get; set; }
}
查询不会填充这些参数。但是,我可以为边缘添加ORID列表:
public class Person
{
public string Name { get; set; }
public List<ORID> out_Lives { get; set; }
public List<ORID> out_Owns { get; set; }
}
public class Country
{
public string Name { get; set; }
public List<ORID> in_Lives { get; set; }
}
public class Car
{
public string Name { get; set; }
public List<ORID> in_Owns { get; set; }
}
如果我只是尝试查询顶点,这是不起作用的:
db.Select().From("Person").ToList<Person>();
库的不同部分显然需要不同类型的边缘列表。(如果我没有记错的话,其中一部分要求它们是List
s,一部分要求他们是HashSet
s,一部分则要求他们有一个无参数构造函数。)但如果我专门查询边,它确实有效:
db.Select("Name").As("Name")
.Also("out('Owns')").As("out_Owns")
.Also("out('Lives')").As("out_Lives")
.From("Person").ToList<Person>();
不幸的是,ORID
类没有我需要的任何信息;它只是一个引用,我可以在其他查询中使用它。
我的最后一个解决方案是尝试创建填充程序属性,该属性将基于RID查询数据库,只是为了看看我是否可以正常工作:
public class Person
{
public string Name { get; set; }
public List<ORID> out_Owns { get; set; }
public List<ORID> out_Lives { get; set; }
public IEnumerable<Car> Owns
{
get
{
if (owns != null &&
out_Owns != null && owns.Count() == out_Owns.Count) return owns;
owns = new List<Car>();
if (out_Owns == null || out_Owns.Count == 0) return owns;
using (ODatabase db = new ODatabase("Cars"))
{
owns = db.Select("Name").As("Name")
.Also("in('Owns')").As("in_Owns")
.From(out_Owns.First()).ToList<Car>();
}
return owns;
}
}
public Country Lives
{
get
{
if (lives != null) return lives;
if (out_Lives == null || out_Lives.Count == 0) return null;
using (ODatabase db = new ODatabase("Cars"))
{
lives = db.Select("Name").As("Name")
.Also("in('Lives')").As("in_Lives")
.From(out_Lives.First()).ToList<Country>()
.FirstOrDefault();
}
}
}
private IEnumerable<Car> owns;
private Country lives;
}
public class Country
{
public string Name { get; set; }
public List<ORID> in_Lives { get; set; }
public IEnumerable<Person> Lives
{
get
{
if (lives != null &&
in_Lives != null && lives.Count() == in_Lives.Count) return lives;
lives = new List<Person>();
if (in_Lives == null || in_Lives.Count == 0) return lives;
using (ODatabase db = new ODatabase("Cars"))
{
StringBuilder sb = new StringBuilder();
foreach (ORID id in in_Lives)
{
sb.AppendFormat("{0},", id);
}
if (sb.Length > 0)
{
sb.Remove(sb.Length - 1, 1);
}
lives = db.Select("Name").As("Name")
.Also("out('Owns')").As("out_Owns")
.Also("out('Lives')").As("out_Lives")
.From(string.Format("[{0}]", sb)).ToList<Person>();
}
return lives;
}
}
private IEnumerable<Person> lives;
}
public class Car
{
public string Name { get; set; }
public List<ORID> in_Owns { get; set; }
public Person Owns
{
get
{
if (owns != null) return owns;
if (in_Owns == null || in_Owns.Count == 0) return null;
using (ODatabase db = new ODatabase("Cars"))
{
owns = db.Select("Name").As("Name")
.Also("out('Owns')").As("out_Owns")
.Also("out('Lives')").As("out_Lives")
.From(in_Owns.First()).ToList<Person>()
.FirstOrDefault();
}
}
}
private Person owns;
}
这起作用,但它看起来像是非常可怕的代码。我正在get
访问器中查询数据库(尽管缓存结果在一定程度上缓解了这个问题),并且这样做需要了解链接顶点的属性,这意味着在编译时无法捕捉到。这些问题只会随着数据库规模的增加而增加,包括记录数量(我们将要迁移的MySQL表中有一个表目前有700多万行)和属性数量(尽管它可能会被划分为几个类,但其中一个表有100多列)。
我希望能够简单地调用db.Select().From("MyVertex").ToList<MyVertex>();
并获得MyVertex
对象的列表,这些对象具有来自图中顶点的边的属性。这个图书馆能做到这一点吗?对于任何C#库,这可能吗?
向OrientDB-NET.binary 推送补丁
你能试试最新的版本吗?
查找此示例
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Orient.Client;
namespace Orient.Tests.Issues
{
// http://stackoverflow.com/questions/26661636/orientdb-net-binary-for-models
[TestClass]
public class StackOverflow_q_26661636
{
TestDatabaseContext _context;
ODatabase _database;
[TestInitialize]
public void Init()
{
_context = new TestDatabaseContext();
_database = new ODatabase(TestConnection.GlobalTestDatabaseAlias);
_database.Create.Class<Person>().Extends<OVertex>().CreateProperties().Run();
_database.Create.Class<Country>().Extends<OVertex>().CreateProperties().Run();
_database.Create.Class<Car>().Extends<OVertex>().CreateProperties().Run();
_database.Create.Class("Owns").Extends<OEdge>().Run();
_database.Create.Class("Lives").Extends<OEdge>().Run();
}
[TestCleanup]
public void Cleanup()
{
_database.Dispose();
_context.Dispose();
}
[TestMethod]
[TestCategory("Stackoverflow")]
public void q_26661636()
{
var lukaPerson = new Person { Name = "Luca" };
var lpV = _database.Create.Vertex(lukaPerson).Run();
var ferrariModenaCar = new Car { Name = "Ferrari Modena" };
var fmcV = _database.Create.Vertex(ferrariModenaCar).Run();
var bmwCar = new Car { Name = "BMW" };
var bmwcV = _database.Create.Vertex(bmwCar).Run();
var lp_fmcE = _database.Create.Edge("Owns").From(lpV.ORID).To(fmcV.ORID).Run();
var lp_bmwcE = _database.Create.Edge("Owns").From(lpV.ORID).To(bmwcV.ORID).Run();
var countryUS = new Country { Name = "US" };
var uscV = _database.Create.Vertex(countryUS).Run();
var lp_uscE = _database.Create.Edge("Lives").From(lpV.ORID).To(uscV.ORID).Run();
var countryUK = new Country { Name = "UK" };
var ukcV = _database.Create.Vertex(countryUK).Run();
var pl = _database.Select().From<Person>().ToList<Person>().FirstOrDefault(p => p.Name == lukaPerson.Name);
Assert.IsNotNull(pl);
Assert.AreEqual(lukaPerson.Name, pl.Name);
Assert.AreEqual(1, pl.out_Lives.Count);
Assert.AreEqual(2, pl.out_Owns.Count);
}
}
public class Person
{
public string Name { get; set; }
public List<ORID> out_Lives { get; set; }
public List<ORID> out_Owns { get; set; }
}
public class Country
{
public string Name { get; set; }
public List<ORID> in_Lives { get; set; }
}
public class Car
{
public string Name { get; set; }
public List<ORID> in_Owns { get; set; }
}
}
所以,正如@Roman所说,图形数据库的设计并不是为了实现我想要的目标。然而,我已经开发了一些扩展方法,使用traverse
无论如何都能产生结果。
作为先决条件,与此解决方案一起使用的所有模型都需要扩展ABaseModel
,与ABaseModel
位于同一命名空间中,并具有无参数构造函数。基本类:
using Orient.Client;
namespace MyApplication
{
public abstract class ABaseModel
{
public ORID ORID { get; set; }
public int OVersion { get; set; }
public ORecordType OType { get; set; }
public short OClassId { get; set; }
public string OClassName { get; set; }
}
}
这只是为扩展方法提供了一个公共基础,并在使用TypeMapper
进行映射时包括模型的所有保留属性。
完成后,方法Traverse<T>(this ODatabase, string)
和Traverse<T>(this ODatabase, ORID)
扩展了ODatabase
以提供所需的功能:
using Orient.Client;
using Orient.Client.Mapping;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
namespace MyApplication
{
/// <summary>
/// Provides extension methods for <see cref="Orient.Client.ODatabase"/>
/// </summary>
public static class DatabaseExtensions
{
private const int SINGLE_RID_TARGET_PATTERN_INDEX = 3;
private static readonly string[] legalTargets = {
@"^(?:class:)?[a-zA-Z][a-zA-Z0-9]*$", // Class
@"^cluster:'d+$", // Root cluster
@"^'[(?:#'d+:'d+'s*,?'s*)*(?:#'d+:'d+)']$", // Array of RIDs
@"^#'d+:'d+$" // Single root record RID
};
/// <summary>
/// Fills out a collection of models of type <typeparamref name="T"/> using <c>traverse</c>. <paramref name="db"/> must be open.
/// </summary>
/// <remarks>
/// <para>Note that <c>traverse</c> can be slow, and <c>select</c> may be more appropriate. See
/// http://www.orientechnologies.com/docs/last/orientdb.wiki/SQL-Traverse.html#should-i-use-traverse-or-select
/// </para>
/// <para>Lightweight edges are not followed when populating model properties. Make sure to use "heavyweight" edges with either
/// <c>alter property MyEdgeClass.out MANDATORY=true</c> and <c>alter property MyEdgeClass.in MANDATORY=true</c>, or else
/// use <c>alter database custom useLightweightEdges=false</c>.</para>
/// </remarks>
/// <typeparam name="T">The model type. Must extend <see cref="ABaseModel"/>, have a parameterless constructor, and most importantly it must be in the same
/// namespace as <see cref="ABaseModel"/>.</typeparam>
/// <param name="db">The database to query</param>
/// <param name="from">A class, cluster, RID list, or RID to traverse. RIDs are in the form <c>#clusterId:clusterPosition</c>. Lists are in the form
/// <c>[RID,RID,...]</c> with one or more elements (whitespace is ignored). Clusters are in the form <c>cluster:clusterName</c> or <c>cluster:clusterId</c>.</param>
/// <exception cref="System.ArgumentException">If <paramref name="from"/> is an invalid format</exception>
/// <returns>An enumerable collection of models of type <typeparamref name="T"/>. Public instance properties of the models will have their values populated
/// based on all non-lightweight edges in the traversal.</returns>
public static IEnumerable<T> Traverse<T>(this ODatabase db, string from) where T : ABaseModel, new()
{
// Sanity check on target
bool matches = false;
foreach (string pattern in legalTargets)
{
if (Regex.IsMatch(from, pattern))
{
matches = true;
break;
}
}
if (!matches)
{
throw new ArgumentException("Traverse target must be a class, cluster, RID list, or single RID.", "from");
}
bool fromSingleRecord = Regex.IsMatch(from, legalTargets[SINGLE_RID_TARGET_PATTERN_INDEX]);
// Traverse DB
string sql = string.Format("traverse * from {0}", from);
List<ODocument> result = db.Query(sql);
DatabaseTraversal traversal = new DatabaseTraversal(db, result);
// Process result
IEnumerable<T> models = traversal.ToModel<T>();
if (fromSingleRecord)
{
// Either Traverse(ORID) was called, or client code called Traverse with an RID string -- return a single element
models = models.Where(m => m.ORID.ToString().Equals(from));
}
return models;
}
/// <summary>
/// Fills out a model of type <typeparamref name="T"/> using <c>traverse</c>. <paramref name="db"/> must be open.
/// </summary>
/// <remarks>
/// <para>Note that <c>traverse</c> can be slow, and <c>select</c> may be more appropriate. See
/// http://www.orientechnologies.com/docs/last/orientdb.wiki/SQL-Traverse.html#should-i-use-traverse-or-select
/// </para>
/// <para>Lightweight edges are not followed when populating model properties. Make sure to use "heavyweight" edges with either
/// <c>alter property MyEdgeClass.out MANDATORY=true</c> and <c>alter property MyEdgeClass.in MANDATORY=true</c>, or else
/// use <c>alter database custom useLightweightEdges=false</c>.</para>
/// </remarks>
/// <typeparam name="T">The model type. Must extend <see cref="ABaseModel"/>, have a parameterless constructor, and most importantly it must be in the same
/// namespace as <see cref="ABaseModel"/>.</typeparam>
/// <param name="db">The database to query</param>
/// <param name="from">The root RID to traverse.</param>
/// <returns>A model representing the record indicated by <paramref name="from"/>.</returns>
public static T Traverse<T>(this ODatabase db, ORID from) where T : ABaseModel, new()
{
// Traverse<T>(from.ToString()) is guaranteed to have 0 or 1 elements
return db.Traverse<T>(from.ToString()).SingleOrDefault();
}
/// <summary>
/// Helper class for traversing <see cref="Orient.Client.ODatabase"/>
/// </summary>
private class DatabaseTraversal
{
private IEnumerable<ODocument> documents;
private IEnumerable<OEdge> edges;
private IDictionary<ORID, ODocument> documentMap;
private static readonly Func<Type, bool> isModelPropertyEnumerableHelper = pType => typeof(System.Collections.IEnumerable).IsAssignableFrom(pType);
private static readonly Func<PropertyInfo, string> isModelPropertyHelper = pInfo =>
{
string alias = pInfo.Name;
OProperty propertyAlias = pInfo.GetCustomAttributes(typeof(OProperty)).Where(attr => !string.IsNullOrEmpty(((OProperty)attr).Alias)).SingleOrDefault() as OProperty;
if (propertyAlias != null)
{
alias = propertyAlias.Alias;
}
return alias;
};
private static readonly Action<dynamic, dynamic, string> setPropertiesHelper = (parent, child, className) =>
{
PropertyInfo[] properties = parent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty | BindingFlags.GetProperty);
PropertyInfo propertySingle = properties.Where(prop => IsModelProperty(prop, className)).SingleOrDefault();
PropertyInfo propertyCollection = properties.Where(prop => IsModelCollectionProperty(prop, className)).SingleOrDefault();
if (propertySingle != null)
{
propertySingle.SetValue(parent, child);
}
else if (propertyCollection != null)
{
dynamic propertyValue = propertyCollection.GetValue(parent);
if (propertyValue == null)
{
Type listOfT = typeof(List<>).MakeGenericType(propertyCollection.PropertyType.GenericTypeArguments[0]);
IEnumerable collection = (IEnumerable)Activator.CreateInstance(listOfT);
propertyValue = collection;
propertyCollection.SetValue(parent, collection);
}
propertyValue.Add(child);
}
};
/// <summary>
/// Create new <see cref="DatabaseTraversal"/> object. <paramref name="database"/> must be open.
/// </summary>
/// <param name="database">Database to traverse. Required for discovering edges.</param>
/// <param name="documents">Documents produced by <c>traverse * from $target</c></param>
public DatabaseTraversal(ODatabase database, IEnumerable<ODocument> documents)
{
this.documents = documents;
documentMap = documents.ToDictionary<ODocument, ORID>(doc => doc.ORID);
// Need to know which RIDs in documentMap are edges
edges = database.Select().From("E").ToList<OEdge>().Where(edge => documentMap.ContainsKey(edge.ORID));
}
/// <summary>
/// Populate model object(s)
/// </summary>
/// <typeparam name="T">Type of model to return</typeparam>
/// <returns>A collection of model objects which appear in the traversal.</returns>
public IEnumerable<T> ToModel<T>() where T : ABaseModel, new()
{
if (documents.Count() == 0) return null;
IDictionary<ORID, ABaseModel> models = new Dictionary<ORID, ABaseModel>();
foreach (OEdge e in edges)
{
ODocument outDoc = documentMap[e.OutV];
ODocument inDoc = documentMap[e.InV];
dynamic outModel, inModel;
bool containsOutId = models.ContainsKey(outDoc.ORID);
bool containsInId = models.ContainsKey(inDoc.ORID);
// Set the value for the models that edge is pointing into/out of
if (containsOutId)
{
outModel = models[outDoc.ORID];
}
else
{
outModel = GetNewPropertyModel(typeof(T).Namespace, outDoc.OClassName);
MapProperties(outDoc, outModel);
models.Add(outModel.ORID, outModel);
}
if (containsInId)
{
inModel = models[inDoc.ORID];
}
else
{
inModel = GetNewPropertyModel(typeof(T).Namespace, inDoc.OClassName);
MapProperties(inDoc, inModel);
models.Add(inDoc.ORID, inModel);
}
// Set the property values for outModel to inModel if they exist
setPropertiesHelper(outModel, inModel, e.OClassName);
setPropertiesHelper(inModel, outModel, e.OClassName);
}
// Return models of type T
IEnumerable<T> result = models.Select(kvp => kvp.Value).Where(model => model.OClassName.Equals(typeof(T).Name)).Cast<T>();
return result;
}
/// <summary>
/// Map non-edge properties of the vertex to the model
/// </summary>
/// <typeparam name="T">The model type</typeparam>
/// <param name="document">The vertex</param>
/// <param name="resultObj">The model object</param>
private static void MapProperties<T>(ODocument document, T resultObj)
{
(TypeMapperBase.GetInstanceFor(typeof(T)) as TypeMapper<T>).ToObject(document, resultObj);
}
/// <summary>
/// Create a new instance of a model type
/// </summary>
/// <param name="nSpace">The model's namespace</param>
/// <param name="modelName">The model's class name</param>
/// <returns>A newly-initialized instance of the class <c>nSpace.modelName</c></returns>
private static dynamic GetNewPropertyModel(string nSpace, string modelName)
{
Type modelClass = Type.GetType(string.Format("{0}.{1}", nSpace, modelName));
return modelClass.GetConstructor(Type.EmptyTypes).Invoke(null);
}
/// <summary>
/// Checks whether the given property or its alias is a vertex's class name and is not enumerable
/// </summary>
/// <param name="currentProperty">The property to compare name/alias against. Aliases should be set with <see cref="Orient.Client.OProperty"/></param>
/// <param name="name">The vertex class name to compare against</param>
/// <returns><see langword="true"/> if <paramref name="currentProperty"/> is named <paramref name="namne"/> or has an <see cref="Orient.Client.OProperty"/>
/// attribute with an alias of <paramref name="name"/>, and <paramref name="currentProperty"/> is not a collection type.</returns>
private static bool IsModelProperty(PropertyInfo currentProperty, string name)
{
string alias = isModelPropertyHelper(currentProperty);
return !isModelPropertyEnumerableHelper(currentProperty.PropertyType) && alias.Equals(name);
}
/// <summary>
/// Checks whether the given property or its alias is a vertex's class name and is enumerable
/// </summary>
/// <param name="currentProperty">The property to compare name/alias against. Aliases should be set with <see cref="Orient.Client.OProperty"/></param>
/// <param name="name">The vertex class name to compare against</param>
/// <returns><see langword="true"/> if <paramref name="currentProperty"/> is named <paramref name="namne"/> or has an <see cref="Orient.Client.OProperty"/>
/// attribute with an alias of <paramref name="name"/>, and <paramref name="currentProperty"/> is a collection type.</returns>
private static bool IsModelCollectionProperty(PropertyInfo currentProperty, string name)
{
string alias = isModelPropertyHelper(currentProperty);
return isModelPropertyEnumerableHelper(currentProperty.PropertyType) && alias.Equals(name);
}
}
}
}
用法示例:
Person luca = db.Traverse<Person>(new ORID("#12:0"));
// luca.Name == "Luca"
// luca.Owns != null
// luca.Owns.Count == 1
// luca.Owns[0].Name == "Ferrari Modena"
// luca.Owns[0].Owner == luca
// luca.Lives != null
// luca.Lives.Name == "UK"
// luca.Lives.Residents != null
// luca.Lives.Residents.Count == 2
// luca.Lives.Residents[0] == luca
// luca.Lives.Residents[1].Lives.Residents[0] == luca
// luca.Lives.Residents[1].Owns == null -> because there is no edge to any Car