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>();

库的不同部分显然需要不同类型的边缘列表。(如果我没有记错的话,其中一部分要求它们是Lists,一部分要求他们是HashSets,一部分则要求他们有一个无参数构造函数。)但如果我专门查询边,它确实有效:

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.模型二进制

向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