如何编写抽象数据库接口以支持多种数据库类型?


12

如何开始在其较大的应用程序中设计一个抽象类,该类可以与多种类型的数据库(例如MySQL,SQLLite,MSSQL等)接口?

这个设计模式叫什么,它从哪里开始呢?

假设您需要编写一个具有以下方法的类

public class Database {
   public DatabaseType databaseType;
   public Database (DatabaseType databaseType){
      this.databaseType = databaseType;
   }

   public void SaveToDatabase(){
       // Save some data to the db
   }
   public void ReadFromDatabase(){
      // Read some data from db
   }
}

//Application
public class Foo {
    public Database db = new Database (DatabaseType.MySQL);
    public void SaveData(){
        db.SaveToDatabase();
    }
}

我唯一能想到的是每个Database方法中的if语句

public void SaveToDatabase(){
   if(databaseType == DatabaseType.MySQL){

   }
   else if(databaseType == DatabaseType.SQLLite){

   }
}

Answers:


11

您想要的是应用程序使用的接口的多种实现

像这样:

public interface IDatabase
{
    void SaveToDatabase();
    void ReadFromDatabase();
}

public class MySQLDatabase : IDatabase
{
   public MySQLDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //MySql implementation
   }
   public void ReadFromDatabase(){
      //MySql implementation
   }
}

public class SQLLiteDatabase : IDatabase
{
   public SQLLiteDatabase ()
   {
      //init stuff
   }

   public void SaveToDatabase(){
       //SQLLite implementation
   }
   public void ReadFromDatabase(){
      //SQLLite implementation
   }
}

//Application
public class Foo {
    public IDatabase db = GetDatabase();

    public void SaveData(){
        db.SaveToDatabase();
    }

    private IDatabase GetDatabase()
    {
        if(/*some way to tell if should use MySql*/)
            return new MySQLDatabase();
        else if(/*some way to tell if should use MySql*/)
            return new SQLLiteDatabase();

        throw new Exception("You forgot to configure the database!");
    }
}

至于IDatabase在运行时在应用程序中设置正确实现的更好方法,则应研究“ 工厂方法 ”和“ 依赖注入 ”之类的东西。


25

卡勒布(Caleb)的步伐正确,但实际上是错误的。他的Foo班级既充当数据库外观又充当工厂。这是两项职责,不应放在同一类中。


这个问题,特别是在数据库环境中,已经被问了太多遍了。在这里,我将尽力向您展示使用抽象(使用接口)的好处,以使您的应用程序更少耦合,功能更广泛。

在进一步阅读之前,建议您阅读并获得对依赖注入的基本了解,如果您还不了解的话。您可能还需要检查Adapter设计模式,这基本上是将实现细节隐藏在接口的公共方法后面的意思。

依赖注入与Factory设计模式相结合,是IoC原理的一部分,是对Strategy设计模式进行编码的一种简便方法。

不要打电话给我们,我们会打电话给您。(又称好莱坞原则)。


使用抽象解耦应用程序

1.制作抽象层

您可以创建一个接口(或抽象类,如果您使用的是C ++语言),则可以向该接口添加通用方法。因为接口和抽象类都具有无法直接使用它们的行为,但是您必须实现(在接口的情况下)或扩展(在抽象类的情况下),所以代码本身已经暗示,您将需要具有特定的实现来完成由接口或抽象类给出的合同。

您的(非常简单的示例)数据库接口可能看起来像这样(DatabaseResult或DbQuery类分别是您自己的表示数据库操作的实现):

public interface Database
{
    DatabaseResult DoQuery(DbQuery query);
    void BeginTransaction();
    void RollbackTransaction();
    void CommitTransaction();
    bool IsInTransaction();
}

因为这是一个接口,所以它本身并没有做任何事情。因此,您需要一个类来实现此接口。

public class MyMySQLDatabase : Database
{
    private readonly CSharpMySQLDriver _mySQLDriver;

    public MyMySQLDatabase(CSharpMySQLDriver mySQLDriver)
    {
        _mySQLDriver = mySQLDriver;
    }

    public DatabaseResult DoQuery(DbQuery query)
    {
        // This is a place where you will use _mySQLDriver to handle the DbQuery
    }

    public void BeginTransaction()
    {
        // This is a place where you will use _mySQLDriver to begin transaction
    }

    public void RollbackTransaction()
    {
    // This is a place where you will use _mySQLDriver to rollback transaction
    }

    public void CommitTransaction()
    {
    // This is a place where you will use _mySQLDriver to commit transaction
    }

    public bool IsInTransaction()
    {
    // This is a place where you will use _mySQLDriver to check, whether you are in a transaction
    }
}

现在,您有了一个实现的类Database,该接口才变得有用。

2.使用抽象层

在您的应用程序中的某个位置,您有一个方法,我们称此方法SecretMethod为好玩,而在此方法内,您必须使用数据库,因为您要获取一些数据。

现在,您有了一个不能直接创建的接口(呃,那我该如何使用它),但是却有了一个class MyMySQLDatabase,可以使用new关键字构造它。

大!我想使用数据库,所以我将使用MyMySQLDatabase

您的方法可能如下所示:

public void SecretMethod()
{
    var database = new MyMySQLDatabase(new CSharpMySQLDriver());

    // you will use the database here, which has the DoQuery,
    // BeginTransaction, RollbackTransaction and CommitTransaction methods
}

这个不好。您可以直接在此方法中创建一个类,如果您在中创建了一个类SecretMethod,则可以安全地假设您将在其他30种方法中进行相同的操作。如果您想将更MyMySQLDatabase改为其他类,例如MyPostgreSQLDatabase,则必须在所有30种方法中进行更改。

另一个问题是,如果创建MyMySQLDatabase失败,则该方法将永远无法完成,因此将无效。

我们首先通过将的创建MyMySQLDatabase作为参数传递给方法来重构其创建(这称为依赖注入)。

public void SecretMethod(MyMySQLDatabase database)
{
    // use the database here
}

这为您解决了MyMySQLDatabase无法创建对象的问题。因为SecretMethod期望值是一个有效的MyMySQLDatabase对象,所以如果发生某些事情并且该对象永远不会传递给它,则该方法将永远不会运行。那完全没问题。


在某些应用中,这可能就足够了。您可能会满意,但让我们将其重构为更好。

另一个重构的目的

您现在可以看到SecretMethod使用MyMySQLDatabase对象。假设您从MySQL迁移到MSSQL。您实际上并不想改变您内部的所有逻辑SecretMethod,该方法对作为参数传递的变量调用BeginTransactionand CommitTransaction方法database,因此您将创建一个新类MyMSSQLDatabase,该类也具有BeginTransactionand CommitTransaction方法。

然后,继续SecretMethod进行以下操作。

public void SecretMethod(MyMSSQLDatabase database)
{
    // use the database here
}

而由于类MyMSSQLDatabaseMyMySQLDatabase有相同的方法,你不需要改变任何东西,它仍然可以工作。

等一下!

您有一个实现的Database接口,MyMySQLDatabase也有一个MyMSSQLDatabase类,该类具有与完全相同的方法MyMySQLDatabase,也许MSSQL驱动程序也可以实现该Database接口,因此将其添加到定义中。

public class MyMSSQLDatabase : Database { }

但是,如果我以后MyMSSQLDatabase不再使用,因为我改用PostgreSQL,该怎么办?我将不得不再次替换SecretMethod?的定义。

是的,你会的。听起来不对。现在我们知道,MyMSSQLDatabase并且MyMySQLDatabase有相同的方法,并都实现了Database接口。因此,您将其重构SecretMethod为如下所示。

public void SecretMethod(Database database)
{
    // use the database here
}

请注意,SecretMethod无论您是使用MySQL,MSSQL还是PotgreSQL,它都不再知道。它知道它使用数据库,但不关心特定的实现。

现在,如果您要创建新的数据库驱动程序(例如,对于PostgreSQL),则完全不需要更改SecretMethod。您将制作一个MyPostgreSQLDatabase,使其实现该Database接口,完成PostgreSQL驱动程序的编码并使其工作之后,您将创建其实例并将其注入SecretMethod

3.获得所需的实施 Database

在调用之前,您仍然必须确定SecretMethod所需Database接口的实现(无论是MySQL,MSSQL还是PostgreSQL)。为此,您可以使用工厂设计模式。

public class DatabaseFactory
{
    private Config _config;

    public DatabaseFactory(Config config)
    {
        _config = config;
    }

    public Database getDatabase()
    {
        var databaseType = _config.GetDatabaseType();

        Database database = null;

        switch (databaseType)
        {
        case DatabaseEnum.MySQL:
            database = new MyMySQLDatabase(new CSharpMySQLDriver());
            break;
        case DatabaseEnum.MSSQL:
            database = new MyMSSQLDatabase(new CSharpMSSQLDriver());
            break;
        case DatabaseEnum.PostgreSQL:
            database = new MyPostgreSQLDatabase(new CSharpPostgreSQLDriver());
            break;
        default:
            throw new DatabaseDriverNotImplementedException();
            break;
        }

        return database;
    }
}

如您所见,工厂从配置文件中知道要使用哪种数据库类型(同样,Config该类可能是您自己的实现)。

理想情况下,您将DatabaseFactory在依赖注入容器的内部。然后,您的过程可能如下所示。

public class ProcessWhichCallsTheSecretMethod
{
    private DIContainer _di;
    private ClassWithSecretMethod _secret;

    public ProcessWhichCallsTheSecretMethod(DIContainer di, ClassWithSecretMethod secret)
    {
        _di = di;
        _secret = secret;
    }

    public void TheProcessMethod()
    {
        Database database = _di.Factories.DatabaseFactory.getDatabase();
        _secret.SecretMethod(database);
    }
}

看,您在创建特定数据库类型的过程中无处可寻。不仅如此,您根本不会创建任何东西。您正在GetDatabaseDatabaseFactory存储在依赖项注入容器(_di变量)中的对象调用一个方法,该方法将Database根据您的配置返回正确的接口实例。

如果在使用PostgreSQL的3周后要返回MySQL,则打开一个配置文件并将DatabaseDriverfield 的值从更改DatabaseEnum.PostgreSQLDatabaseEnum.MySQL。您完成了。突然,应用程序的其余部分通过更改一行来再次正确使用MySQL。


如果您仍然不感到惊讶,我建议您多花一点时间研究IoC。如何不根据配置而是根据用户输入做出某些决定。这种方法称为策略模式,尽管可以并且已在企业应用程序中使用,但在开发计算机游戏时更经常使用。


爱你的答案,大卫。但是,像所有这些答案一样,它也没有描述如何将其付诸实践。真正的问题不是要抽象出调用不同数据库引擎的能力,而是真正的SQL语法。以您的DbQuery对象为例。假设该对象包含要执行的SQL查询字符串的成员,那么如何使它通用?
DonBoitnott

1
@DonBoitnott我认为您永远不需要通用的东西。您通常希望在应用程序层(域,服务,持久性)之间引入抽象,您可能还希望为模块引入抽象,您可能想向正在为大型项目开发的小型但可重用和高度可定制的库中引入抽象,等等。您可以将所有内容抽象到接口,但这几乎没有必要。很难给出一个全部答案,因为可悲的是,确实没有一个答案,它来自需求。
安迪

2
明白了 但是我真的是真的。一旦有了抽象类,就可以调用_secret.SecretMethod(database);一个如何协调所有工作的事实,因为现在我SecretMethod仍然必须知道我在使用哪个数据库才能使用正确的SQL方言。 ?您已经非常努力地使大多数代码对该事实一无所知,但是在第11个小时,您必须再次知道。我现在处于这种情况下,试图找出其他人如何解决了这个问题。
DonBoitnott

@DonBoitnott我不知道你的意思,我现在明白了。您可以使用一个接口代替DbQuery该类的具体实现,提供该接口的实现,并改用该接口,并拥有一个工厂来构建IDbQuery实例。我认为您不需要DatabaseResult该类的通用类型,您总是可以期望数据库的结果以类似的方式进行格式化。这里的事情是,在处理数据库和原始SQL时,您的应用程序已经处于较低级别(在DAL和存储库之后),因此不需要...
Andy

...通用方法了。
安迪
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.