Dizi parametrelerini saklı bir prosedüre geçirme


53

Bir sürü kayıt kapanarak (1000'ler) ve üzerinde çalışan bir işlemim var ve işim bittiğinde, çok sayıda işlemi işlenmiş olarak işaretlemem gerekiyor. Bunu büyük bir kimlik listesiyle belirtebilirim. "Döngüdeki güncellemeler" modelinden kaçınmaya çalışıyorum, bu yüzden bu kimlik çantasını MS SQL Server 2008'de depolanan proc'a göndermek için daha etkili bir yol bulmak istiyorum.

Teklif # 1 - Tablo Değerli Parametreler. Yalnızca bir kimlik alanı olan bir tablo türü tanımlayabilir ve güncellenmesi için bir kimlikle dolu tablo gönderebilirim.

Teklif # 2 - proc gövdesinde OPENXML () ile XML parametresi (varchar).

Teklif # 3 - Liste ayrıştırma. Mümkünse hantal ve hataya açık göründüğü için bundan kaçınmayı tercih ederim.

Bunlar arasında herhangi bir tercih veya özlediğim fikirler?


Büyük kimlik listesini nasıl alıyorsunuz?
Larry Coleman

Onları başka bir saklı proc ile "veri yükü" verileriyle birlikte aşağı çekiyorum. Yine de tüm verileri güncellememe gerek yok - sadece belirli kayıtlardaki bir bayrağı güncelleyin.
D. Lambert,

Yanıtlar:


42

Bu konuda şimdiye kadarki en iyi makaleler Erland Sommarskog'a aittir:

Tüm seçenekleri kapsar ve oldukça iyi açıklar.

Cevabın kısalığı için özür dilerim ama Erland'ın Diziler ile ilgili makalesi Joe Celko'nun ağaçlar ve diğer SQL muameleleri hakkındaki kitapları gibi :)


23

StackOverflow'ta bunun birçok yaklaşımı kapsayan harika bir tartışması var . Ben SQL Server 2008+ için tercih birdir kullanmak tablo değerli parametreleri . Bu aslında SQL Server'ın probleminize olan çözümü - değerler listesinden saklı bir prosedüre geçilmesi.

Bu yaklaşımın avantajları:

  • Tüm parametrelerinizi 1 parametre olarak geçirdikten sonra bir saklı yordam çağrısı yapın.
  • tablo girişi yapılandırılmış ve güçlü bir şekilde yazılmıştır
  • XML dizge oluşturma / ayrıştırma ya da işleme yok
  • tablo girişini filtrelemek, birleştirmek veya herhangi bir şekilde kolayca kullanmak

Ancak, not alın: TVP'leri ADO.NET veya ODBC kullanan bir saklı yordam çağırırsanız ve SQL Server Profiler ile etkinliğe bakarsanız, her satır için bir tane olmak üzere SQL Server'ın TVP'yi INSERTyüklemek için birkaç ifade aldığını fark edeceksiniz. TVP’de ise prosedür çağrısına devam edilir. Bu, tasarım gereğidir . Bu toplu INSERTişlem her prosedür çağrıldığında derlenmelidir ve küçük bir ek yük oluşturur. Bununla birlikte, bu genel giderle bile, TVP'ler hala çoğu kullanım senaryosunda performans ve kullanılabilirlik açısından diğer yaklaşımları ortadan kaldırmaktadır .

Daha fazla bilgi edinmek istiyorsanız, Erland Sommarskog, tablo değerindeki parametrelerin nasıl çalıştığını ve birkaç örnek sunduğunu tam olarak gösterir .

İşte benim önerdiğim başka bir örnek:

CREATE TYPE id_list AS TABLE (
    id int NOT NULL PRIMARY KEY
);
GO

CREATE PROCEDURE [dbo].[tvp_test] (
      @param1           INT
    , @customer_list    id_list READONLY
)
AS
BEGIN
    SELECT @param1 AS param1;

    -- join, filter, do whatever you want with this table 
    -- (other than modify it)
    SELECT *
    FROM @customer_list;
END;
GO

DECLARE @customer_list id_list;

INSERT INTO @customer_list (
    id
)
VALUES (1), (2), (3), (4), (5), (6), (7);

EXECUTE [dbo].[tvp_test]
      @param1 = 5
    , @customer_list = @customer_list
;
GO

DROP PROCEDURE dbo.tvp_test;
DROP TYPE id_list;
GO

Bunu çalıştırdığımda hata alıyorum: Msg 2715, Seviye 16, Durum 3, Prosedür tvp_test, Satır 4 [Toplu Başlat Satırı 4] Sütun, parametre veya değişken # 2: id_list veri türü bulunamıyor. '@Customer_list' parametresi veya değişkeni geçersiz bir veri türüne sahip. Mesaj 1087, Seviye 16, Durum 1, İşlem tvp_test, Satır 13 [Toplu Başlangıç ​​Satırı 4] "değişkenini" @customer_list "olarak bildirmelidir.
Damian

@Damian - Başındaki CREATE TYPEifade başarılı bir şekilde çalıştı mı? Hangi SQL Server sürümünü kullanıyorsunuz?
Nick Chammas

SP kodunda bu cümleyle satır içi `SELECT @ param1 AS param1; ' . Amaç nedir? Kullanmaz veya param1 kullanmazsınız, neden bunu SP başlığında bir parametre olarak kullandınız?
EAmez

@EAmez - Bu sadece rastgele bir örnekti. Mesele @customer_listdeğil @param1. Örnek sadece farklı parametre türlerini karıştırabileceğinizi göstermektedir.
Nick Chammas

21

Tüm konu üzerinde tartışılmıştır Erland Sommarskog tarafından kesin makalesinde: "SQL Server Dizileri ve Listesi" . Hangi sürümü seçeceğinizi seçin.

TVP'lerin geri kalanını koyduğu SQL Server 2008 öncesi için Özet

  • CSV, istediğiniz gibi bölün (genellikle bir sayı tablosu kullanırım)
  • XML ve ayrıştırma (SQL Server 2005+ ile daha iyi)
  • İstemcide geçici bir tablo oluşturun

Yazı, başka teknikleri ve düşünceyi görmek için yine de okumaya değer.

Düzenleme: başka bir yerde büyük listeler için geç cevap : Dizi parametrelerini saklı bir yordama geçirme


14

Bu partiye geç kaldığımı biliyorum, fakat geçmişte böyle bir sorun yaşadım, 100 bin bigint numara göndermek zorunda kaldım ve birkaç kıyaslama yaptım. Onları ikilik formatta, resim olarak gönderdik - bu 100K numara için her şeyden daha hızlıydı.

İşte eski (SQL Server 2005) kodum:

SELECT  Number * 8 + 1 AS StartFrom ,
        Number * 8 + 8 AS MaxLen
INTO    dbo.ParsingNumbers
FROM    dbo.Numbers
GO

CREATE FUNCTION dbo.ParseImageIntoBIGINTs ( @BIGINTs IMAGE )
RETURNS TABLE
AS RETURN
    ( SELECT    CAST(SUBSTRING(@BIGINTs, StartFrom, 8) AS BIGINT) Num
      FROM      dbo.ParsingNumbers
      WHERE     MaxLen <= DATALENGTH(@BIGINTs)
    )
GO

Aşağıdaki kod, tamsayıları ikili bir blob içine dolduruyor. Buradaki bayt sırasını tersine çeviriyorum:

static byte[] UlongsToBytes(ulong[] ulongs)
{
int ifrom = ulongs.GetLowerBound(0);
int ito   = ulongs.GetUpperBound(0);
int l = (ito - ifrom + 1)*8;
byte[] ret = new byte[l];
int retind = 0;
for(int i=ifrom; i<=ito; i++)
{
ulong v = ulongs[i];
ret[retind++] = (byte) (v >> 0x38);
ret[retind++] = (byte) (v >> 0x30);
ret[retind++] = (byte) (v >> 40);
ret[retind++] = (byte) (v >> 0x20);
ret[retind++] = (byte) (v >> 0x18);
ret[retind++] = (byte) (v >> 0x10);
ret[retind++] = (byte) (v >> 8);
ret[retind++] = (byte) v;
}
return ret;
}

9

Seni SO'ya yönlendirmekle ya da buraya cevaplamak arasındayım. 'Çünkü bu neredeyse bir programlama sorusu. Ama kullandığım bir çözümü zaten aldığımdan beri ... Bunu göndereceğim;)

Bunun işleyiş şekli, virgülle ayrılmış bir dizgiyi (basit bölme, CSV stil bölmeleri yapmaz) depolanan yordama bir varchar (4000) olarak beslemeniz ve bu listeyi bu işleve beslemeniz ve kullanışlı bir tablo çıkarmanızdır. sadece varchars bir tablo.

Bu, sadece işlenmesini istediğiniz kimlikleri değerlerini göndermenize izin verir ve bu noktada basit bir birleştirme yapabilirsiniz.

Alternatif olarak, bir CLR DataTable ile bir şeyler yapabilir ve bunu besleyebilirsiniz, ancak bu desteklenmesi biraz daha fazla olur ve herkes CSV listelerini anlar.

USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER FUNCTION [dbo].[splitListToTable] (@list      nvarchar(MAX), @delimiter nchar(1) = N',')
      RETURNS @tbl TABLE (value     varchar(4000)      NOT NULL) AS
/*
http://www.sommarskog.se/arrays-in-sql.html
This guy is apparently THE guy in SQL arrays and lists 

Need an easy non-dynamic way to split a list of strings on input for comparisons

Usage like thus:

DECLARE @sqlParam VARCHAR(MAX)
SET @sqlParam = 'a,b,c'

SELECT * FROM (

select 'a' as col1, '1' as col2 UNION
select 'a' as col1, '2' as col2 UNION
select 'b' as col1, '3' as col2 UNION
select 'b' as col1, '4' as col2 UNION
select 'c' as col1, '5' as col2 UNION
select 'c' as col1, '6' as col2 ) x 
WHERE EXISTS( SELECT value FROM splitListToTable(@sqlParam,',') WHERE x.col1 = value )

*/
BEGIN
   DECLARE @endpos   int,
           @startpos int,
           @textpos  int,
           @chunklen smallint,
           @tmpstr   nvarchar(4000),
           @leftover nvarchar(4000),
           @tmpval   nvarchar(4000)

   SET @textpos = 1
   SET @leftover = ''
   WHILE @textpos <= datalength(@list) / 2
   BEGIN
      SET @chunklen = 4000 - datalength(@leftover) / 2
      SET @tmpstr = @leftover + substring(@list, @textpos, @chunklen)
      SET @textpos = @textpos + @chunklen

      SET @startpos = 0
      SET @endpos = charindex(@delimiter, @tmpstr)

      WHILE @endpos > 0
      BEGIN
         SET @tmpval = ltrim(rtrim(substring(@tmpstr, @startpos + 1,
                                             @endpos - @startpos - 1)))
         INSERT @tbl (value) VALUES(@tmpval)
         SET @startpos = @endpos
         SET @endpos = charindex(@delimiter, @tmpstr, @startpos + 1)
      END

      SET @leftover = right(@tmpstr, datalength(@tmpstr) / 2 - @startpos)
   END

   INSERT @tbl(value) VALUES (ltrim(rtrim(@leftover)))
   RETURN
END

Şey, özellikle virgülle ayrılmış bir listeden kaçınmaya çalışıyordum, böylece böyle bir şey yazmak zorunda kalmamıştım, ama zaten yazıldığından beri, bu çözümü tekrar karışıma atmak zorunda kalacağım. ;-)
D. Lambert

1
Denedim ve doğru en kolay diyorum. Kodun saniyelerinde C # ile virgülle ayrılmış bir liste tükürebilirsiniz ve yeterince hızlı bir şekilde bu fonksiyona (sproc'unuza girdikten sonra) atabilirsiniz ve hakkında düşünmek zorunda bile değilsiniz. ~ Ve bir işlev kullanmak istemediğini söylediğini biliyorum, ama bence en basit yol (belki de en etkili değil)
jcolebrand

5

Çeşitli SQL Server saklı yordamları tarafından işlenmek üzere uygulamamızdan gönderilen düzenli olarak 1000 satır kümesi ve 10000 satır satırı alıyorum.

Performans taleplerini karşılamak için TVP'leri kullanıyoruz, ancak varsayılan işlem modunda bazı performans sorunlarının üstesinden gelmek için dbDataReader'ın kendi özetini uygulamanız gerekir. Bu istek için kapsam dışında oldukları için nasıl ve nedenlere girmeyeceğim.

10.000'den fazla "satır" ile performans gösteren bir XML uygulaması bulamadığım için XML işlemeyi düşünmedim.

Liste işleme, tek boyutlu ve çift boyutlu tally (sayılar) tablo işleme ile gerçekleştirilebilir. Bunları çeşitli alanlarda başarıyla kullandık, ancak iyi yönetilen TVP'ler, birkaç yüzden fazla satır olduğunda daha iyi performans gösteriyor.

SQL Server işlemesi ile ilgili tüm seçeneklerde olduğu gibi seçim seçiminizi kullanım modeline göre yapmak zorundasınız.


5

Sonunda bazı TableValuedParameters yapma şansım oldu ve harika çalışıyorlar. Bu yüzden, nasıl kullandığımı gösteren bir çok lot kodu yapıştıracağım. .AĞ)

Ayrıca not: Bir hizmet için bazı kodlar yazıyorum ve diğer sınıfta önceden tanımlanmış çok sayıda kod biti var, ancak bunu bir konsol uygulaması olarak yazıyorum, böylece hata ayıklayabiliyorum, bu yüzden hepsini dolandırdım Konsol uygulaması Affedersiniz kodlama stilim (kodlanmış bağlantı dizeleri gibi) "atmak için bir tane inşa etmek" gibi. Nasıl List<customObject>kullandığımı göstermek ve saklı yordamda kullanabileceğim bir tablo olarak kolayca veritabanına itmek istedim . Aşağıdaki C # ve TSQL kodu:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using a;

namespace a.EventAMI {
    class Db {
        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static void Update(List<Current> currents) {
            const string CONSTR = @"just a hardwired connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );
            cmd.Parameters.Add( "@CurrentTVP", SqlDbType.Structured ).Value = Converter.GetDataTableFromIEnumerable( currents, typeof( Current ) ); //my custom converter class

            try {
                using ( con ) {
                    con.Open();
                    cmd.ExecuteNonQuery();
                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }
        }
    }
    class Current {
        public string Identifier { get; set; }
        public string OffTime { get; set; }
        public DateTime Off() {
            return Convert.ToDateTime( OffTime );
        }

        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static List<Current> GetAll() {
            List<Current> l = new List<Current>();

            const string CONSTR = @"just a hardcoded connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );

            try {
                using ( con ) {
                    con.Open();
                    using ( SqlDataReader reader = cmd.ExecuteReader() ) {
                        while ( reader.Read() ) {
                            l.Add(
                                new Current {
                                    Identifier = reader[0].ToString(),
                                    OffTime = reader[1].ToString()
                                } );
                        }
                    }

                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }

            return l;
        }
    }
}

-------------------
the converter class
-------------------
using System;
using System.Collections;
using System.Data;
using System.Reflection;

namespace a {
    public static class Converter {
        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable) {
            return GetDataTableFromIEnumerable( aIEnumerable, null );
        }

        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable, Type baseType) {
            DataTable returnTable = new DataTable();

            if ( aIEnumerable != null ) {
                //Creates the table structure looping in the in the first element of the list
                object baseObj = null;

                Type objectType;

                if ( baseType == null ) {
                    foreach ( object obj in aIEnumerable ) {
                        baseObj = obj;
                        break;
                    }

                    objectType = baseObj.GetType();
                } else {
                    objectType = baseType;
                }

                PropertyInfo[] properties = objectType.GetProperties();

                DataColumn col;

                foreach ( PropertyInfo property in properties ) {
                    col = new DataColumn { ColumnName = property.Name };
                    if ( property.PropertyType == typeof( DateTime? ) ) {
                        col.DataType = typeof( DateTime );
                    } else if ( property.PropertyType == typeof( Int32? ) ) {
                        col.DataType = typeof( Int32 );
                    } else {
                        col.DataType = property.PropertyType;
                    }
                    returnTable.Columns.Add( col );
                }

                //Adds the rows to the table

                foreach ( object objItem in aIEnumerable ) {
                    DataRow row = returnTable.NewRow();

                    foreach ( PropertyInfo property in properties ) {
                        Object value = property.GetValue( objItem, null );
                        if ( value != null )
                            row[property.Name] = value;
                        else
                            row[property.Name] = "";
                    }

                    returnTable.Rows.Add( row );
                }
            }
            return returnTable;
        }

    }
}

USE [Database]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROC [dbo].[Event_Update]
    @EventCurrentTVP    Event_CurrentTVP    READONLY
AS

/****************************************************************
    author  cbrand
    date    
    descrip I'll ask you to forgive me the anonymization I've made here, but hope this helps
    caller  such and thus application
****************************************************************/

BEGIN TRAN Event_Update

DECLARE @DEBUG INT

SET @DEBUG = 0 /* test using @DEBUG <> 0 */

/*
    Replace the list of outstanding entries that are still currently disconnected with the list from the file
    This means remove all existing entries (faster to truncate and insert than to delete on a join and insert, yes?)
*/
TRUNCATE TABLE [database].[dbo].[Event_Current]

INSERT INTO [database].[dbo].[Event_Current]
           ([Identifier]
            ,[OffTime])
SELECT [Identifier]
      ,[OffTime]
  FROM @EventCurrentTVP

IF (@@ERROR <> 0 OR @DEBUG <> 0) 
BEGIN
ROLLBACK TRAN Event_Update
END
ELSE
BEGIN
COMMIT TRAN Event_Update
END

USE [Database]
GO

CREATE TYPE [dbo].[Event_CurrentTVP] AS TABLE(
    [Identifier] [varchar](20) NULL,
    [OffTime] [datetime] NULL
)
GO

Ayrıca, (eğer bu soruya rastlayan tüm okuyucular için) bunu teklif ediyorsanız, lütfen yapıcı tutun;) Kodlama tarzım hakkında yapıcı eleştiri yapacağım. . İnşallah bu kod parçası ile birileri List<Current>db'de ve List<T>uygulamalarında bir tablo olarak tanımladığım gibi nasıl kullanabileceklerini görebilirler .


3

Ben de # 1 öneriyle gideceğim ya da alternatif olarak, sadece işlenmiş kimlikleri tutan bir çalışma masası oluşturacağım. İşleme sırasında bu tabloya ekleyin, sonra bir kere bittiğinde aşağıdakine benzer bir proc arayın:

BEGIN TRAN

UPDATE dt
SET processed = 1
FROM dataTable dt
JOIN processedIds pi ON pi.id = dt.id;

TRUNCATE TABLE processedIds

COMMIT TRAN

Çok fazla kesici uç kullanacaksınız, ancak küçük bir masaya gelecekler, o yüzden hızlı olmalı. Eklerinizi ADO.net veya kullandığınız veri bağdaştırıcısı kullanarak da gruplayabilirsiniz.


2

Soru başlığı, bir uygulamadan verileri saklı yordama aktarma görevini içerir. Bu kısım soru organı tarafından hariç tutulur, ancak bunu da cevaplamaya çalışayım.

Sql-server-2008 bağlamında etiketlerde belirtildiği gibi, E. Sommarskog Dizileri ve Listelerinde SQL Server 2008'deki bir başka harika makale var . BTW Marian'ın cevabında değinilen makalesinde buldum.

Sadece bağlantıyı vermek yerine, içerik listesini alıntı yapıyorum:

  • Giriş
  • Arka fon
  • T-SQL'de Tablo Değerli Parametreler
  • Tablo-Değerli Parametreleri ADO .NET'ten Geçmek
    • Liste Kullanımı
    • DataTable Kullanma
    • DataReader Kullanımı
    • Son açıklamalar
  • Diğer API'lerden Tablo Değerli Parametreleri Kullanma
    • ODBC
    • OLE DB
    • ADO
    • LINQ ve Varlık Çerçevesi
    • JDBC
    • PHP
    • Perl
    • API'nız TVP'leri Desteklemiyorsa Ne
  • Performans ile ilgili önemli noktalar
    • Sunucu tarafı
    • İstemci tarafı
    • Birincil Anahtar mı Değil mi?
  • Teşekkürler ve Geribildirim
  • Revizyon Geçmişi

Burada belirtilen tekniklerin ötesinde, bazı durumlarda toplu iş ve toplu işlerin genel durumla ilgili olarak belirtilmeyi hak ettiğini hissediyorum.


1

Dizi parametrelerini saklı bir prosedüre geçirme

MS SQL 2016 için en son sürüm

MS SQL 2016 ile yeni bir fonksiyon tanıtıyorlar: SPLIT_STRING () çoklu değerleri ayrıştırmak için.

Bu probleminizi kolayca çözebilir.

MS SQL Eski Sürüm için

Eski sürümü kullanıyorsanız, şu adımı takip edin:

İlk önce bir işlev yap:

 ALTER FUNCTION [dbo].[UDF_IDListToTable]
 (
    @list          [varchar](MAX),
    @Seperator     CHAR(1)
  )
 RETURNS @tbl TABLE (ID INT)
 WITH 

 EXECUTE AS CALLER
 AS
  BEGIN
    DECLARE @position INT
    DECLARE @NewLine CHAR(2) 
    DECLARE @no INT
    SET @NewLine = CHAR(13) + CHAR(10)

    IF CHARINDEX(@Seperator, @list) = 0
    BEGIN
    INSERT INTO @tbl
    VALUES
      (
        @list
      )
END
ELSE
BEGIN
    SET @position = 1
    SET @list = @list + @Seperator
    WHILE CHARINDEX(@Seperator, @list, @position) <> 0
    BEGIN
        SELECT @no = SUBSTRING(
                   @list,
                   @position,
                   CHARINDEX(@Seperator, @list, @position) - @position
               )

        IF @no <> ''
            INSERT INTO @tbl
            VALUES
              (
                @no
              )

        SET @position = CHARINDEX(@Seperator, @list, @position) + 1
    END
END
RETURN
END

Bunu yaptıktan sonra, dizginizi ayırıcı ile bu işleve geçirin.

Umarım bu sana yardımcı olur. :-)


-1

Bunu "create type table" oluşturmak için kullanın. kullanıcı için basit bir örnek

CREATE TYPE unit_list AS TABLE (
    ItemUnitId int,
    Amount float,
    IsPrimaryUnit bit
);

GO
 CREATE TYPE specification_list AS TABLE (
     ItemSpecificationMasterId int,
    ItemSpecificationMasterValue varchar(255)
);

GO
 declare @units unit_list;
 insert into @units (ItemUnitId, Amount, IsPrimaryUnit) 
  values(12,10.50, false), 120,100.50, false), (1200,500.50, true);

 declare @spec specification_list;
  insert into @spec (ItemSpecificationMasterId,temSpecificationMasterValue) 
   values (12,'test'), (124,'testing value');

 exec sp_add_item "mytests", false, @units, @spec


//Procedure definition
CREATE PROCEDURE sp_add_item
(   
    @Name nvarchar(50),
    @IsProduct bit=false,
    @UnitsArray unit_list READONLY,
    @SpecificationsArray specification_list READONLY
)
AS


BEGIN
    SET NOCOUNT OFF     

    print @Name;
    print @IsProduct;       
    select * from @UnitsArray;
    select * from @SpecificationsArray;
END
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.