Heim > Java > javaLernprogramm > Die Verwendung von Sperren und Abfragetypen in Datenbankoperationen des Hibernate-Frameworks von Java

Die Verwendung von Sperren und Abfragetypen in Datenbankoperationen des Hibernate-Frameworks von Java

高洛峰
Freigeben: 2016-12-27 13:35:51
Original
1810 Leute haben es durchsucht

Ruhezustand und Datenbanksperren
1. Warum Sperren verwenden?

Um zu verstehen, warum der Sperrmechanismus existiert, müssen Sie zunächst das Konzept von Transaktionen verstehen.
Eine Transaktion ist eine Reihe zusammenhängender Vorgänge in der Datenbank, die ACID-Merkmale aufweisen müssen:

A (Atomizität): entweder alle erfolgreich oder alle werden widerrufen.

C (Konsistenz): Um die Konsistenz der Datenbank aufrechtzuerhalten.

I (Isolation): Wenn verschiedene Transaktionen mit denselben Daten arbeiten, müssen sie über einen eigenen Datenraum verfügen.

D (Dauerhaftigkeit): Sobald eine Transaktion erfolgreich beendet wird, müssen die Aktualisierungen, die sie an der Datenbank vornimmt, dauerhaft beibehalten werden.

Unser häufig verwendetes relationales Datenbank-RDBMS implementiert diese Merkmale von Transaktionen. Unter anderem werden Atomizität,
Konsistenz und Haltbarkeit durch Protokolle garantiert. Die Isolation wird durch den
Sperrmechanismus erreicht, auf den wir uns heute konzentrieren. Deshalb brauchen wir den Sperrmechanismus.

Was sind die möglichen Konsequenzen, wenn es keine Sperre und keine Kontrolle über die Isolation gibt?

Update verloren: Die von Transaktion 1 übermittelten Daten wurden von Transaktion 2 überschrieben.

Dirty Read: Transaktion 2 fragt die nicht festgeschriebenen Daten von Transaktion 1 ab.

Falsches Lesen: Transaktion 2 fragt die neuen Daten ab, die von Transaktion 1 übermittelt wurden.

Nicht wiederholbares Lesen: Transaktion 2 fragt die aktualisierten Daten ab, die von Transaktion 1 übermittelt wurden.

Schauen wir uns das Hibernate-Beispiel an. Zwei Threads starten zwei Transaktionen, um dieselbe Datenzeile col_id=1 in der Tabelle tb_account zu bearbeiten.

package com.cdai.orm.hibernate.annotation; 
  
import java.io.Serializable; 
  
import javax.persistence.Column; 
import javax.persistence.Entity; 
import javax.persistence.Id; 
import javax.persistence.Table; 
  
@Entity
@Table(name = "tb_account") 
public class Account implements Serializable { 
  
  private static final long serialVersionUID = 5018821760412231859L; 
  
  @Id
  @Column(name = "col_id") 
  private long id; 
    
  @Column(name = "col_balance") 
  private long balance; 
  
  public Account() { 
  } 
    
  public Account(long id, long balance) { 
    this.id = id; 
    this.balance = balance; 
  } 
  
  public long getId() { 
    return id; 
  } 
  
  public void setId(long id) { 
    this.id = id; 
  } 
  
  public long getBalance() { 
    return balance; 
  } 
  
  public void setBalance(long balance) { 
    this.balance = balance; 
  } 
  
  @Override
  public String toString() { 
    return "Account [id=" + id + ", balance=" + balance + "]"; 
  } 
    
}
Nach dem Login kopieren
package com.cdai.orm.hibernate.transaction; 
  
import org.hibernate.Session; 
import org.hibernate.SessionFactory; 
import org.hibernate.Transaction; 
import org.hibernate.cfg.AnnotationConfiguration; 
  
import com.cdai.orm.hibernate.annotation.Account; 
  
public class DirtyRead { 
  
  public static void main(String[] args) { 
  
    final SessionFactory sessionFactory = new AnnotationConfiguration(). 
        addFile("hibernate/hibernate.cfg.xml").        
        configure(). 
        addPackage("com.cdai.orm.hibernate.annotation"). 
        addAnnotatedClass(Account.class). 
        buildSessionFactory(); 
      
    Thread t1 = new Thread() { 
        
      @Override
      public void run() { 
        Session session1 = sessionFactory.openSession(); 
        Transaction tx1 = null; 
        try { 
          tx1 = session1.beginTransaction(); 
          System.out.println("T1 - Begin trasaction"); 
          Thread.sleep(500); 
            
          Account account = (Account)  
              session1.get(Account.class, new Long(1)); 
          System.out.println("T1 - balance=" + account.getBalance()); 
          Thread.sleep(500); 
            
          account.setBalance(account.getBalance() + 100); 
          System.out.println("T1 - Change balance:" + account.getBalance()); 
            
          tx1.commit(); 
          System.out.println("T1 - Commit transaction"); 
          Thread.sleep(500); 
        } 
        catch (Exception e) { 
          e.printStackTrace(); 
          if (tx1 != null) 
            tx1.rollback(); 
        }  
        finally { 
          session1.close(); 
        } 
      } 
        
    }; 
      
    // 3.Run transaction 2 
    Thread t2 = new Thread() { 
        
      @Override
      public void run() { 
        Session session2 = sessionFactory.openSession(); 
        Transaction tx2 = null; 
        try { 
          tx2 = session2.beginTransaction(); 
          System.out.println("T2 - Begin trasaction"); 
          Thread.sleep(500); 
            
          Account account = (Account)  
              session2.get(Account.class, new Long(1)); 
          System.out.println("T2 - balance=" + account.getBalance()); 
          Thread.sleep(500); 
            
          account.setBalance(account.getBalance() - 100); 
          System.out.println("T2 - Change balance:" + account.getBalance()); 
            
          tx2.commit(); 
          System.out.println("T2 - Commit transaction"); 
          Thread.sleep(500); 
        } 
        catch (Exception e) { 
          e.printStackTrace(); 
          if (tx2 != null) 
            tx2.rollback(); 
        }  
        finally { 
          session2.close(); 
        } 
      } 
        
    }; 
      
    t1.start(); 
    t2.start(); 
      
    while (t1.isAlive() || t2.isAlive()) { 
      try { 
        Thread.sleep(2000L); 
      } catch (InterruptedException e) { 
      } 
    } 
      
    System.out.println("Both T1 and T2 are dead."); 
    sessionFactory.close(); 
      
  } 
  
}
Nach dem Login kopieren
Transaktion 1 verringert col_balance um 100 und Transaktion 2 verringert es um 100, das Endergebnis kann 0 sein, oder

es kann 200 sein, eine Aktualisierung von Transaktion 1 oder 2 kann verloren gehen. Die Protokollausgabe bestätigt dies auch. Die Protokolle der Transaktionen 1 und 2
werden kreuzweise gedruckt.

T1 - Begin trasaction
T2 - Begin trasaction
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ where account0_.col_id=?
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ where account0_.col_id=?
T1 - balance=100
T2 - balance=100
T2 - Change balance:0
T1 - Change balance:200
Hibernate: update tb_account set col_balance=? where col_id=?
Hibernate: update tb_account set col_balance=? where col_id=?
T1 - Commit transaction
T2 - Commit transaction
Both T1 and T2 are dead.
Nach dem Login kopieren
Es ist ersichtlich, dass Isolation ein Thema ist, das sorgfältig geprüft werden muss, und es ist notwendig, Sperren zu verstehen.

2. Wie viele Arten von Schlössern gibt es?

Übliche Sperren sind gemeinsame Sperren, Aktualisierungssperren und exklusive Sperren.

1. Gemeinsame Sperre: Wird für Lesedatenvorgänge verwendet, sodass andere Transaktionen gleichzeitig lesen können. Wenn eine Transaktion eine Select-Anweisung ausführt,

weist die Datenbank der Transaktion automatisch eine gemeinsame Sperre zu, um die gelesenen Daten zu sperren.
2. Exklusive Sperre: Wird zum Ändern von Daten verwendet. Andere Transaktionen können sie nicht lesen oder ändern. Die Datenbank wird automatisch zugewiesen, wenn die Transaktion Einfügen,
Aktualisieren und Löschen ausführt.
3. Aktualisierungssperre: Wird verwendet, um Deadlocks zu vermeiden, die durch gemeinsame Sperren während Aktualisierungsvorgängen verursacht werden. Beispielsweise halten die Transaktionen 1 und 2 gleichzeitig gemeinsame Sperren und warten auf den Erhalt exklusiver Sperren. Beim Ausführen der Aktualisierung erhält die Transaktion zunächst die Aktualisierungssperre und aktualisiert dann die Aktualisierungssperre
auf eine exklusive Sperre, wodurch ein Deadlock vermieden wird.

Darüber hinaus können diese Sperren auf verschiedene Objekte in der Datenbank angewendet werden, d. h. diese Sperren können unterschiedliche Granularitäten haben.

Zum Beispiel Sperren auf Datenbankebene, Sperren auf Tabellenebene, Sperren auf Seitenebene, Sperren auf Schlüsselebene und Sperren auf Zeilenebene.


Es gibt also viele Arten von Sperren. Es ist zu schwierig, so viele Sperren vollständig zu beherrschen und flexibel zu nutzen.

Was tun? Glücklicherweise ist der Sperrmechanismus für uns normale Benutzer transparent. Die Datenbank fügt automatisch die entsprechenden

Sperren hinzu und stuft verschiedene Sperren automatisch hoch und herab. Alles, was wir tun müssen, ist
lernen, die Isolationsstufe entsprechend den unterschiedlichen Geschäftsanforderungen festzulegen.

3. Wie stelle ich die Isolationsstufe ein?


Im Allgemeinen bietet das Datenbanksystem vier Transaktionsisolationsstufen zur Auswahl:

1. Serialisierbar (serialisierbar): Wenn zwei Transaktionen gleichzeitig dieselben Daten bearbeiten, Transaktion 2 Ich kann nur anhalten und warten.

2. Wiederholbares Lesen: Transaktion 1 kann die neu eingefügten Daten von Transaktion 2 sehen, aber keine Aktualisierungen der

vorhandenen Daten.


3. Read Committed (festgeschriebene Daten lesen): Transaktion 1 kann die neu eingefügten und aktualisierten Daten von Transaktion 2 sehen.

4. Nicht festgeschriebene Daten lesen: Transaktion 1 kann die nicht festgeschriebenen Daten von Transaktion 2 sehen und aktualisieren.


4. Sperren in der Anwendung

Wenn die Datenbank die Isolationsstufe „Read Commited“ annimmt, können in der Anwendung pessimistische Sperren oder optimistische Sperren verwendet werden.

1. Pessimistische Sperre: Es wird davon ausgegangen, dass auf die von der aktuellen Transaktion verarbeiteten Daten definitiv von anderen Transaktionen zugegriffen wird. Geben Sie daher pessimistisch eine exklusive Sperre im Anwendungsprogramm

an, um die Datenressourcen zu sperren. Die folgenden Formen werden in MySQL und Oracle unterstützt:


ermöglicht explizit die Verwendung einer exklusiven Sperre zum Sperren der abgefragten Datensätze. Andere Transaktionen müssen die von

gesperrten Daten abfragen, aktualisieren oder löschen bis die Transaktion abgeschlossen ist.
select ... for update
Nach dem Login kopieren

Im Ruhezustand können Sie LockMode.UPGRADE beim Laden übergeben, um pessimistische Sperren zu verwenden. Ändern Sie das vorherige Beispiel, indem Sie
einen weiteren LockMode-Parameter beim Get-Methodenaufruf der Transaktionen 1 und 2 übergeben. Wie aus dem Protokoll hervorgeht, laufen die Transaktionen 1 und 2

nicht mehr übergreifend ab. Transaktion 2 wartet auf das Ende von Transaktion 1, bevor sie Daten liest, sodass der endgültige col_balance-Wert 100 ist, was korrekt ist

.


Hibernate führt SQL für SQLServer 2005 aus:
package com.cdai.orm.hibernate.transaction; 
  
import org.hibernate.LockMode; 
import org.hibernate.Session; 
import org.hibernate.SessionFactory; 
import org.hibernate.Transaction; 
  
import com.cdai.orm.hibernate.annotation.Account; 
import com.cdai.orm.hibernate.annotation.AnnotationHibernate; 
  
public class UpgradeLock { 
  
  @SuppressWarnings("deprecation") 
  public static void main(String[] args) { 
  
    final SessionFactory sessionFactory = AnnotationHibernate.createSessionFactory();  
  
    // Run transaction 1 
    Thread t1 = new Thread() { 
        
      @Override
      public void run() { 
        Session session1 = sessionFactory.openSession(); 
        Transaction tx1 = null; 
        try { 
          tx1 = session1.beginTransaction(); 
          System.out.println("T1 - Begin trasaction"); 
          Thread.sleep(500); 
            
          Account account = (Account)  
              session1.get(Account.class, new Long(1), LockMode.UPGRADE); 
          System.out.println("T1 - balance=" + account.getBalance()); 
          Thread.sleep(500); 
            
          account.setBalance(account.getBalance() + 100); 
          System.out.println("T1 - Change balance:" + account.getBalance()); 
            
          tx1.commit(); 
          System.out.println("T1 - Commit transaction"); 
          Thread.sleep(500); 
        } 
        catch (Exception e) { 
          e.printStackTrace(); 
          if (tx1 != null) 
            tx1.rollback(); 
        }  
        finally { 
          session1.close(); 
        } 
      } 
        
    }; 
      
    // Run transaction 2 
    Thread t2 = new Thread() { 
        
      @Override
      public void run() { 
        Session session2 = sessionFactory.openSession(); 
        Transaction tx2 = null; 
        try { 
          tx2 = session2.beginTransaction(); 
          System.out.println("T2 - Begin trasaction"); 
          Thread.sleep(500); 
            
          Account account = (Account)  
              session2.get(Account.class, new Long(1), LockMode.UPGRADE); 
          System.out.println("T2 - balance=" + account.getBalance()); 
          Thread.sleep(500); 
            
          account.setBalance(account.getBalance() - 100); 
          System.out.println("T2 - Change balance:" + account.getBalance()); 
            
          tx2.commit(); 
          System.out.println("T2 - Commit transaction"); 
          Thread.sleep(500); 
        } 
        catch (Exception e) { 
          e.printStackTrace(); 
          if (tx2 != null) 
            tx2.rollback(); 
        }  
        finally { 
          session2.close(); 
        } 
      } 
        
    }; 
      
    t1.start(); 
    t2.start(); 
      
    while (t1.isAlive() || t2.isAlive()) { 
      try { 
        Thread.sleep(2000L); 
      } catch (InterruptedException e) { 
      } 
    } 
      
    System.out.println("Both T1 and T2 are dead."); 
    sessionFactory.close(); 
  
  } 
  
}
Nach dem Login kopieren
T1 - Begin trasaction
T2 - Begin trasaction
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=?
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=?
T2 - balance=100
T2 - Change balance:0
Hibernate: update tb_account set col_balance=? where col_id=?
T2 - Commit transaction
T1 - balance=0
T1 - Change balance:100
Hibernate: update tb_account set col_balance=? where col_id=?
T1 - Commit transaction
Both T1 and T2 are dead.
Nach dem Login kopieren

Zeilensperre und Aktualisierungssperre für die ausgewählte Datenzeile mit col_id 1 hinzufügen.

select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=?
Nach dem Login kopieren
2. Optimistische Sperre: Es wird davon ausgegangen, dass auf die von der aktuellen Transaktion verarbeiteten Daten nicht gleichzeitig von anderen Transaktionen zugegriffen wird, sodass sie vollständig auf die Isolationsstufe der Datenbank

angewiesen ist die Schleusenarbeiten verwalten. Verwenden Sie die Versionskontrolle in Ihrer Anwendung, um Parallelitätsprobleme zu vermeiden, die

selten auftreten können.


Verwenden Sie im Ruhezustand die Versionsanmerkung, um das Versionsnummernfeld zu definieren.

Die Verwendung von Sperren und Abfragetypen in Datenbankoperationen des Hibernate-Frameworks von Java

将DirtyLock中的Account对象替换成AccountVersion,其他代码不变,执行出现异常。

package com.cdai.orm.hibernate.transaction; 
  
import javax.persistence.Column; 
import javax.persistence.Entity; 
import javax.persistence.Id; 
import javax.persistence.Table; 
import javax.persistence.Version; 
  
@Entity
@Table(name = "tb_account_version") 
public class AccountVersion { 
  
  @Id
  @Column(name = "col_id") 
  private long id; 
    
  @Column(name = "col_balance") 
  private long balance; 
    
  @Version
  @Column(name = "col_version") 
  private int version; 
  
  public AccountVersion() { 
  } 
  
  public AccountVersion(long id, long balance) { 
    this.id = id; 
    this.balance = balance; 
  } 
  
  public long getId() { 
    return id; 
  } 
  
  public void setId(long id) { 
    this.id = id; 
  } 
  
  public long getBalance() { 
    return balance; 
  } 
  
  public void setBalance(long balance) { 
    this.balance = balance; 
  } 
  
  public int getVersion() { 
    return version; 
  } 
  
  public void setVersion(int version) { 
    this.version = version; 
  } 
    
}
Nach dem Login kopieren

log如下:

T1 - Begin trasaction
T2 - Begin trasaction
Hibernate: select accountver0_.col_id as col1_0_0_, accountver0_.col_balance as col2_0_0_, accountver0_.col_version as col3_0_0_ from tb_account_version accountver0_ where accountver0_.col_id=?
Hibernate: select accountver0_.col_id as col1_0_0_, accountver0_.col_balance as col2_0_0_, accountver0_.col_version as col3_0_0_ from tb_account_version accountver0_ where accountver0_.col_id=?
T1 - balance=1000
T2 - balance=1000
T1 - Change balance:900
T2 - Change balance:1100
Hibernate: update tb_account_version set col_balance=?, col_version=? where col_id=? and col_version=?
Hibernate: update tb_account_version set col_balance=?, col_version=? where col_id=? and col_version=?
T1 - Commit transaction
2264 [Thread-2] ERROR org.hibernate.event.def.AbstractFlushingEventListener - Could not synchronize database state with session
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.cdai.orm.hibernate.transaction.AccountVersion#1]
   at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1934)
   at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2578)
   at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2478)
   at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2805)
   at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:114)
   at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268)
   at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:260)
   at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:180)
   at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
   at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
   at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1206)
   at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:375)
   at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
   at com.cdai.orm.hibernate.transaction.VersionLock$2.run(VersionLock.java:93)
Both T1 and T2 are dead.
Nach dem Login kopieren

由于乐观锁完全将事务隔离交给数据库来控制,所以事务1和2交叉运行了,事务1提交
成功并将col_version改为1,然而事务2提交时已经找不到col_version为0的数据了,所以
抛出了异常。

Die Verwendung von Sperren und Abfragetypen in Datenbankoperationen des Hibernate-Frameworks von Java

Hibernate查询方法比较
Hibernate主要有三种查询方法:

1.HQL (Hibernate Query Language)

和SQL很类似,支持分页、连接、分组、聚集函数和子查询等特性,
但HQL是面向对象的,而不是面向关系数据库中的表。正因查询语句
是面向Domain对象的,所以使用HQL可以获得跨平台的好处,Hibernate
会自动帮我们根据不同的数据库翻译成不同的SQL语句。这在需要支持
多种数据库或者数据库迁移的应用中是十分方便的。

但得到方便的同时,由于SQL语句是由Hibernate自动生成的,所以这不
利于SQL语句的效率优化和调试,当数据量很大时可能会有效率问题,
出了问题也不便于排查解决。

2.QBC/QBE (Query by Criteria/Example)

QBC/QBE是通过组装查询条件或者模板对象来执行查询的。这在需要
灵活地支持许多查询条件自由组合的应用中是比较方便的。同样的问题
是由于查询语句是自由组装的,创建一条语句的代码可能很长,并且
包含许多分支条件,很不便于优化和调试。

3.SQL

Hibernate也支持直接执行SQL的查询方式。这种方式牺牲了Hibernate跨
数据库的优点,手工地编写底层SQL语句,从而获得最好的执行效率,
相对前两种方法,优化和调试方便了一些。

下面来看一组简单的例子。

package com.cdai.orm.hibernate.query; 
  
import java.util.Arrays; 
import java.util.List; 
  
import org.hibernate.Criteria; 
import org.hibernate.Query; 
import org.hibernate.Session; 
import org.hibernate.SessionFactory; 
import org.hibernate.cfg.AnnotationConfiguration; 
import org.hibernate.criterion.Criterion; 
import org.hibernate.criterion.Example; 
import org.hibernate.criterion.Expression; 
  
import com.cdai.orm.hibernate.annotation.Account; 
  
public class BasicQuery { 
  
  public static void main(String[] args) { 
  
    SessionFactory sessionFactory = new AnnotationConfiguration(). 
                      addFile("hibernate/hibernate.cfg.xml").        
                      configure(). 
                      addPackage("com.cdai.orm.hibernate.annotation"). 
                      addAnnotatedClass(Account.class). 
                      buildSessionFactory(); 
  
    Session session = sessionFactory.openSession(); 
  
    // 1.HQL 
    Query query = session.createQuery("from Account as a where a.id=:id"); 
    query.setLong("id", 1); 
    List result = query.list(); 
    for (Object row : result) { 
      System.out.println(row); 
    } 
  
    // 2.QBC 
    Criteria criteria = session.createCriteria(Account.class); 
    criteria.add(Expression.eq("id", new Long(2))); 
    result = criteria.list(); 
    for (Object row : result) { 
      System.out.println(row); 
    } 
      
    // 3.QBE 
    Account example= new Account(); 
    example.setBalance(100); 
    result = session.createCriteria(Account.class). 
            add(Example.create(example)). 
            list(); 
    for (Object row : result) { 
      System.out.println(row); 
    } 
      
    // 4.SQL 
    query = session.createSQLQuery( 
        " select top 10 * from tb_account order by col_id desc "); 
    result = query.list(); 
    for (Object row : result) { 
      System.out.println(Arrays.toString((Object[]) row)); 
  } 
      
    session.close(); 
  } 
  
}
Nach dem Login kopieren
Hibernate: select account0_.col_id as col1_0_, account0_.col_balance as col2_0_ from tb_account account0_ where account0_.col_id=?
Account [id=1, balance=100]
Hibernate: select this_.col_id as col1_0_0_, this_.col_balance as col2_0_0_ from tb_account this_ where this_.col_id=?
Account [id=2, balance=100]
Hibernate: select this_.col_id as col1_0_0_, this_.col_balance as col2_0_0_ from tb_account this_ where (this_.col_balance=?)
Account [id=1, balance=100]
Account [id=2, balance=100]
Hibernate: select top 10 * from tb_account order by col_id desc
[2, 100]
[1, 100]
Nach dem Login kopieren

从log中可以清楚的看到Hibernate对于生成的SQL语句的控制,具体选择
哪种查询方式就要看具体应用了。

更多Java的Hibernate框架数据库操作中锁的使用和查询类型相关文章请关注PHP中文网!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage